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)  
This commit is contained in:
parent
9e1117019a
commit
3c07e01d08
|
|
@ -72,6 +72,8 @@ impl Color32 {
|
||||||
pub const BLUE: Self = Self::from_rgb(0, 0, 255);
|
pub const BLUE: Self = Self::from_rgb(0, 0, 255);
|
||||||
pub const LIGHT_BLUE: Self = Self::from_rgb(0xAD, 0xD8, 0xE6);
|
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 GOLD: Self = Self::from_rgb(255, 215, 0);
|
||||||
|
|
||||||
pub const DEBUG_COLOR: Self = Self::from_rgba_premultiplied(0, 200, 0, 128);
|
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.
|
/// Multiply with 0.5 to make color half as opaque in linear space.
|
||||||
///
|
///
|
||||||
/// This is using linear space, which is not perceptually even.
|
/// 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)),
|
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 {
|
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]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,10 @@ pub struct Frame {
|
||||||
#[doc(alias = "border")]
|
#[doc(alias = "border")]
|
||||||
pub stroke: Stroke,
|
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,
|
pub rounding: Rounding,
|
||||||
|
|
||||||
/// Margin outside the painted frame.
|
/// Margin outside the painted frame.
|
||||||
|
|
@ -269,7 +272,10 @@ impl Frame {
|
||||||
self
|
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]
|
#[inline]
|
||||||
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
|
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
|
||||||
self.rounding = rounding.into();
|
self.rounding = rounding.into();
|
||||||
|
|
@ -423,15 +429,14 @@ impl Frame {
|
||||||
shadow,
|
shadow,
|
||||||
} = *self;
|
} = *self;
|
||||||
|
|
||||||
let fill_rect = self.fill_rect(content_rect);
|
|
||||||
let widget_rect = self.widget_rect(content_rect);
|
let widget_rect = self.widget_rect(content_rect);
|
||||||
|
|
||||||
let frame_shape = Shape::Rect(epaint::RectShape::new(
|
let frame_shape = Shape::Rect(epaint::RectShape::new(
|
||||||
fill_rect,
|
widget_rect,
|
||||||
rounding,
|
rounding,
|
||||||
fill,
|
fill,
|
||||||
stroke,
|
stroke,
|
||||||
epaint::StrokeKind::Outside,
|
epaint::StrokeKind::Inside,
|
||||||
));
|
));
|
||||||
|
|
||||||
if shadow == Default::default() {
|
if shadow == Default::default() {
|
||||||
|
|
|
||||||
|
|
@ -611,7 +611,8 @@ impl Window<'_> {
|
||||||
title_bar.inner_rect.round_to_pixels(ctx.pixels_per_point());
|
title_bar.inner_rect.round_to_pixels(ctx.pixels_per_point());
|
||||||
|
|
||||||
if on_top && area_content_ui.visuals().window_highlight_topmost {
|
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 {
|
if !is_collapsed {
|
||||||
round.se = 0;
|
round.se = 0;
|
||||||
|
|
|
||||||
|
|
@ -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.");
|
.on_hover_text("Apply feathering to smooth out the edges of shapes. Turn off for small performance gain.");
|
||||||
|
|
||||||
if *feathering {
|
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"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2292f0f80bfd3c80055a72eb983549ac2875d36acb333732bd0a67e51b24ae4f
|
oid sha256:57274cec5ee7e5522073249b931ea65ead22752aea1de40666543e765c1b6b85
|
||||||
size 102983
|
size 102929
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ impl Default for DemoGroups {
|
||||||
Box::<super::tests::InputTest>::default(),
|
Box::<super::tests::InputTest>::default(),
|
||||||
Box::<super::tests::LayoutTest>::default(),
|
Box::<super::tests::LayoutTest>::default(),
|
||||||
Box::<super::tests::ManualLayoutTest>::default(),
|
Box::<super::tests::ManualLayoutTest>::default(),
|
||||||
|
Box::<super::tests::TessellationTest>::default(),
|
||||||
Box::<super::tests::WindowResizeTest>::default(),
|
Box::<super::tests::WindowResizeTest>::default(),
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,9 +124,9 @@ impl View for MiscDemoWindow {
|
||||||
)
|
)
|
||||||
.changed()
|
.changed()
|
||||||
{
|
{
|
||||||
self.checklist
|
for check in &mut self.checklist {
|
||||||
.iter_mut()
|
*check = all_checked;
|
||||||
.for_each(|checked| *checked = all_checked);
|
}
|
||||||
}
|
}
|
||||||
for (i, checked) in self.checklist.iter_mut().enumerate() {
|
for (i, checked) in self.checklist.iter_mut().enumerate() {
|
||||||
ui.checkbox(checked, format!("Item {}", i + 1));
|
ui.checkbox(checked, format!("Item {}", i + 1));
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ mod input_event_history;
|
||||||
mod input_test;
|
mod input_test;
|
||||||
mod layout_test;
|
mod layout_test;
|
||||||
mod manual_layout_test;
|
mod manual_layout_test;
|
||||||
|
mod tessellation_test;
|
||||||
mod window_resize_test;
|
mod window_resize_test;
|
||||||
|
|
||||||
pub use clipboard_test::ClipboardTest;
|
pub use clipboard_test::ClipboardTest;
|
||||||
|
|
@ -16,4 +17,5 @@ pub use input_event_history::InputEventHistory;
|
||||||
pub use input_test::InputTest;
|
pub use input_test::InputTest;
|
||||||
pub use layout_test::LayoutTest;
|
pub use layout_test::LayoutTest;
|
||||||
pub use manual_layout_test::ManualLayoutTest;
|
pub use manual_layout_test::ManualLayoutTest;
|
||||||
|
pub use tessellation_test::TessellationTest;
|
||||||
pub use window_resize_test::WindowResizeTest;
|
pub use window_resize_test::WindowResizeTest;
|
||||||
|
|
|
||||||
|
|
@ -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}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d4cfd5191dc7046a782ef2350dc8e0547d2702182badcb15b6b928ce077b76c1
|
oid sha256:23f19871720b67659a7b56cee8a78edc941c4bac86f55efc6fa549f10c4712fb
|
||||||
size 32154
|
size 31754
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4631f841b9e23833505af39eb1c45908013d3b1e1278d477bcedf6a460c71802
|
oid sha256:e4d8fee9fd8e69ecd60ebd0dd41c29b61cc13e7013b1d20ad93d40fc4ed1cc03
|
||||||
size 27163
|
size 27091
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:96e750ebfcc6ec2c130674407388c30cce844977cde19adfebf351fd08698a4f
|
oid sha256:b291e7efd895ab095590285b841903f05dc7d4dadbab7d9001b04a92953c1694
|
||||||
size 81726
|
size 81677
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b03613e597631da3b922ef3d696c4ce74cec41f86a2779fc5b084a316fc9e8e8
|
oid sha256:5d75230689807ae7fb692bac5c9b33ee04c02b9e54963e2d6ada05860157daf6
|
||||||
size 11764
|
size 11705
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:76d77e2df39af248d004a023b238bb61ed598ab2bea3e0c6f2885f9537ec1103
|
oid sha256:f512159108c7681834ae2a52c5e1d4eb4dbe678a509f3dab3384e35d6c8dbee2
|
||||||
size 25988
|
size 25865
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:003d905893b80ffc132a783155971ad3054229e9d6c046e2760c912559a66b3d
|
oid sha256:f2a4f5e67ff6615877794304e8be5a5db647d48e20e4248259179cddefbf4088
|
||||||
size 20869
|
size 20806
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:be0f96c700b7662aab5098f8412dae3676116eeed65e70f6b295dd3375b329d0
|
oid sha256:0bc474a37d6d3a7a08dd41963bf9009c05315de91bb515cc2b19443b79480bff
|
||||||
size 10968
|
size 10723
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:99c94a09c0f6984909481189f9a1415ea90bd7c45e42b4b3763ee36f3d831a65
|
oid sha256:0bff769b3f9e46c5e38170885b2c8301294cbcc1c8ee22c17672205edd509924
|
||||||
size 133231
|
size 133170
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e4fef5fa8661f207bae2c58381e729cdaf77aecc8b3f178caf262dc310e3a490
|
oid sha256:f90f94a842a1d0f1386c3cdd28e60e6ad6efe968f510a11bde418b5bc70a81d2
|
||||||
size 24206
|
size 23897
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7b38828e423195628dcea09b0cbdd66aa4c077334ab7082efd358c7a3297009d
|
oid sha256:fdbdf1159f0ab4579bf9ceefbd8e40ac7af468368ab11ffec8578214b57d867c
|
||||||
size 17827
|
size 17758
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c53403996451da14f7991ea61bd20b96dbc67eb67dd2041975dd6ce5015a6980
|
oid sha256:ce647fd9626f126e7f19b4494bb98a2086071f9c45f7dd6ba4708632e7433a7b
|
||||||
size 22485
|
size 22418
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:63673b070951246b98ca07613fa81496dbfdd10627bac3c9c4356ebff1a36b20
|
oid sha256:a843e6772809a1c904968fac19b49973675e726a3b7727ca1b898ad3b9072b0c
|
||||||
size 64319
|
size 64257
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:374b4095a3c7b827b80d6ab01b945458ae0128a2974c2af8aaf6b7e9fef6b459
|
oid sha256:b437f27c46ddf82e4268d9bb86d33f43c382d1ab3ed45297bc136600cdc9960c
|
||||||
size 32554
|
size 32493
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5e756c90069803bb4d2821fff723877bffffd44b26613f5b06c8a042d6901ca4
|
oid sha256:2cd0bda8b4d3d7f833273097c8bb52cfd8ea63a405d6798b9aeb7002b143ac74
|
||||||
size 36578
|
size 36459
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6efc59cb9908533baa1a7346b359e9e21c5faf0e373dac6fa7db5476e644233d
|
oid sha256:a57c3bf373a283b79188080e2ddf7407c2974655fc5ad59222442e14faae055c
|
||||||
size 17678
|
size 17508
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:eb631bd2608aec6c83f8815b9db0b28627bf82692fd2b1cb793119083b4f8ad1
|
oid sha256:161b5853f528206f2531587753369f2f6f3c52203af668eba0d81a41c2a915e0
|
||||||
size 264496
|
size 264432
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9fee66bab841602862c301a7df3add365ea93fde0ac9b71ca7f9b74a709a68d2
|
oid sha256:118413a5ec56c6589914c5cb59bda2884a4d6acec3b34bbc367bc893c3de8127
|
||||||
size 35576
|
size 35409
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e0870e9e1c9dc718690124a4d490f1061414f15fa40571e571d9319c3b65e74e
|
oid sha256:361791f30739f841c46433f5d16d281ba9d0c52027b4cf156c3ac293aa795f46
|
||||||
size 23709
|
size 23592
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:532dbcbec94bb9c9fb8cc0665646790a459088e98077118b5fbb2112898e1a43
|
oid sha256:81338d7b0412989590bc0808419d93788e972e7ab6f70f481f86bd23263a5395
|
||||||
size 183854
|
size 183821
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3fd584643b68084ec4b65369e08d229e2d389551bbefa59c84ad4b58621593f7
|
oid sha256:41c51350c09360738ca284b05c944a11164e4a614beb95ce4cc962222af33d87
|
||||||
size 117754
|
size 117764
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7e2b854d99c9b17d15ef92188cdac521d7c0635aef9ba457cd3801884a784009
|
oid sha256:794475478cfe2fc954731742165b1f0419a0e64616e72b3766c1e031dbba7ca1
|
||||||
size 26159
|
size 26092
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f0b7dc029de8e669d76f3be5e0800e973c354edcf7cefa239faed07c2cd5e0d5
|
oid sha256:7073aa30f3e7dfd116d9a4f02f6c5a075ce068d2e6f43eaede3c4dd6c56f925e
|
||||||
size 70452
|
size 70439
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:64ba40810a6480e511e8d198b0dfb39e8b220eb2f5592877e27b17ee8d47b9c3
|
oid sha256:4bb3cd05f253c0e109b83a0556b975af7a96d57678e57de3b9fa130cc8a8de1c
|
||||||
size 66387
|
size 66318
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0a8151f5bd01b2cb5183c532d11f57bbb7e8cc1e77a3c4b890537d157922c961
|
oid sha256:5b550769d5ec5d834f89fba56375d10e5f9b3ba703599d90a5b6607aff1c2b06
|
||||||
size 21261
|
size 21194
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:96ce36fcc334793801ab724c711f592faf086a9c98c812684e6b76010e9d1974
|
oid sha256:45f343b55be98976de32bd3c5438212a46b787123ea3096cf8fc6bec99493184
|
||||||
size 59714
|
size 59699
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8155d93b78692ced67ddee4011526ef627838ede78351871df5feef8aa512857
|
oid sha256:8d25f774320ca844fa4dfba5215ed66f067d0f30c6d7c8ad7ec29d97ee7732e0
|
||||||
size 13141
|
size 13073
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bf6da022ab97f9d4b862cc8e91bdfd7f9785a3ab0540aa1c2bd2646bd30a3450
|
oid sha256:540a05c5b1de7e362bcb48a04611323fbc65c8787b8ae852ada8aa145803753d
|
||||||
size 35115
|
size 34968
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:035b35ed053cabd5022d9a97f7d4d5a79d194c9f14594e7602ecd9ef94c24ae5
|
oid sha256:2276b8221389da4f644cad43ce446cd7d28f41820cea36fa3ea860808710053d
|
||||||
size 48053
|
size 47878
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:80a2968e211c83639b60e84b805f1327fb37b87144cada672a403c7e92ace8a8
|
oid sha256:054d606681e08bf61762e5c7d7596c4f483e66ac889795e4be4956103328e0bb
|
||||||
size 48066
|
size 47862
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:86df5dc4b4ddd6f44226242b6d9b5e9f2aacd45193ae9f784fb5084a7a509e0b
|
oid sha256:f7fb29285e53b619f333757052d05f86d973680ac1ce6d1b25d28b7824bbce51
|
||||||
size 43987
|
size 43725
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1138bbc3b7e73cccd555f0fd58c27a5bda4d84484fdc1bd5223fc9802d0c5328
|
oid sha256:be50a396838d4fe29bcfd0807377ad0cda538fea8569acf709da0b13505bf09b
|
||||||
size 44089
|
size 43871
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3562bb87b155b2c406a72a651ffb1991305aa1e15273ce9f32cedc5766318537
|
oid sha256:9c6ce16a8a8c34d882485da6bbe08039fb55f90636da8136f68b1bb9baf0effb
|
||||||
size 554922
|
size 557610
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ba6c0bd127ab02375f2ae5fbc3eeef33a1bdf074cbb180de2733c700b81df3e5
|
oid sha256:a49c052578a46adb41bc02c6d7fdc264ed0ab8ae636cc8a11ac729fe1e48091b
|
||||||
size 771069
|
size 791802
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d85ab6d04059009fd2c3ad8001332b27e710c46c9300f2f1f409b882c49211dc
|
oid sha256:989c55a83b8bc7cce4f459d8b835962377927a98f2bce085e92cba8438438ecd
|
||||||
size 918967
|
size 943736
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a64f1bdec565357fe4dee3acb46b12eeb0492b522fb3bb9697d39dadce2e8c21
|
oid sha256:01ac61dc5bcecf6bf0d13c8399460b1afae652efe6fecc1d0e4b2f27d9f1c5a4
|
||||||
size 1039455
|
size 1046906
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5ca008dca03372bb334564e55fa2d1d25a36751a43df6001a1c1cf3e4db9bcd4
|
oid sha256:3348923ecbad34e385e3ed52ab9b7c88b5d4fc07de00620302d5b191d90a453f
|
||||||
size 1130930
|
size 1140236
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7f695127e7fe6cb3b752a0acd71db85d51d2de68e45051a7afe91f4d960acf27
|
oid sha256:179c17b0405c6e87bd3cafaa7272e3e6d6eefd462b4406cca2a7abfe8af6f2bd
|
||||||
size 1311641
|
size 1317569
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:aa7d25b097911f6b18308bab56d302e3dae9f8f9916f563d5703632a26eda260
|
||||||
|
size 72501
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c39868f184364555ae90fbfc035aa668f61189be7aeee6bec4e45a8de438ad8e
|
||||||
|
size 87661
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:dd029fdc49e6d4078337472c39b9d58bf69073c1b7750c6dd1b7ccd450d52395
|
||||||
|
size 119869
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e698ba12efd129099877248f9630ba983d683e1b495b2523ed3569989341e905
|
||||||
|
size 51735
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:18b81b5cd88372b65b1ecc62e9a5e894960279310b05a1bd5c8df5bffa244ad0
|
||||||
|
size 54922
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:bf5df173431d330e4b6045a72227c2bb7613ec98c63f013ea899a3a57cd6617a
|
||||||
|
size 55522
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:987c162842a08271e833c41a55573d9f30cf045bf7ca3cb03e81d0cc13d5a16e
|
||||||
|
size 36763
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8c5c3055cd190823a4204aa6f23362a88bc5ab5ed5453d9be1b6077dded6cd54
|
||||||
|
size 36809
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a291f3a5724aefc59ba7881f48752ccc826ca5e480741c221d195061f562ccc9
|
oid sha256:946bf96ae558ee7373b50bf11959e82b1f4d91866ec61b04b0336ae170b6f7b2
|
||||||
size 158220
|
size 158553
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,12 @@ pub fn try_image_snapshot_options(
|
||||||
output_path,
|
output_path,
|
||||||
} = options;
|
} = 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
|
// The one that is checked in to git
|
||||||
let snapshot_path = output_path.join(format!("{name}.png"));
|
let snapshot_path = output_path.join(format!("{name}.png"));
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:64fd46da67cab2afae0ea8997a88fb43fd207e24cc3943086d978a8de717320f
|
oid sha256:f65efbf60e190d83d187ec51f3f7811eb55135ef4feb9586e931e8498bc05d64
|
||||||
size 7542
|
size 7430
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,13 @@ impl Mesh {
|
||||||
self.indices.is_empty() && self.vertices.is_empty()
|
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.
|
/// Calculate a bounding rectangle.
|
||||||
pub fn calc_bounds(&self) -> Rect {
|
pub fn calc_bounds(&self) -> Rect {
|
||||||
let mut bounds = Rect::NOTHING;
|
let mut bounds = Rect::NOTHING;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
/// How rounded the corners of things should be.
|
/// 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,
|
/// The rounding uses `u8` to save space,
|
||||||
/// so the amount of rounding is limited to integers in the range `[0, 255]`.
|
/// 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]
|
#[inline]
|
||||||
fn add(self, rhs: Self) -> Self {
|
fn add(self, rhs: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
nw: self.nw + rhs.nw,
|
nw: self.nw.saturating_add(rhs.nw),
|
||||||
ne: self.ne + rhs.ne,
|
ne: self.ne.saturating_add(rhs.ne),
|
||||||
sw: self.sw + rhs.sw,
|
sw: self.sw.saturating_add(rhs.sw),
|
||||||
se: self.se + rhs.se,
|
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]
|
#[inline]
|
||||||
fn add_assign(&mut self, rhs: Self) {
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
*self = Self {
|
*self = Self {
|
||||||
nw: self.nw + rhs.nw,
|
nw: self.nw.saturating_add(rhs.nw),
|
||||||
ne: self.ne + rhs.ne,
|
ne: self.ne.saturating_add(rhs.ne),
|
||||||
sw: self.sw + rhs.sw,
|
sw: self.sw.saturating_add(rhs.sw),
|
||||||
se: self.se + rhs.se,
|
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 {
|
impl std::ops::SubAssign for Rounding {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn sub_assign(&mut self, rhs: Self) {
|
fn sub_assign(&mut self, rhs: Self) {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,16 @@ use crate::*;
|
||||||
pub struct RectShape {
|
pub struct RectShape {
|
||||||
pub rect: Rect,
|
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,
|
pub rounding: Rounding,
|
||||||
|
|
||||||
/// How to fill the rectangle.
|
/// How to fill the rectangle.
|
||||||
|
|
|
||||||
|
|
@ -451,8 +451,9 @@ impl Shape {
|
||||||
}
|
}
|
||||||
Self::Rect(rect_shape) => {
|
Self::Rect(rect_shape) => {
|
||||||
rect_shape.rect = transform * rect_shape.rect;
|
rect_shape.rect = transform * rect_shape.rect;
|
||||||
rect_shape.stroke.width *= transform.scaling;
|
|
||||||
rect_shape.rounding *= transform.scaling;
|
rect_shape.rounding *= transform.scaling;
|
||||||
|
rect_shape.stroke.width *= transform.scaling;
|
||||||
|
rect_shape.blur_width *= transform.scaling;
|
||||||
}
|
}
|
||||||
Self::Text(text_shape) => {
|
Self::Text(text_shape) => {
|
||||||
text_shape.pos = transform * text_shape.pos;
|
text_shape.pos = transform * text_shape.pos;
|
||||||
|
|
@ -472,17 +473,17 @@ impl Shape {
|
||||||
Self::Mesh(mesh) => {
|
Self::Mesh(mesh) => {
|
||||||
Arc::make_mut(mesh).transform(transform);
|
Arc::make_mut(mesh).transform(transform);
|
||||||
}
|
}
|
||||||
Self::QuadraticBezier(bezier_shape) => {
|
Self::QuadraticBezier(bezier) => {
|
||||||
bezier_shape.points[0] = transform * bezier_shape.points[0];
|
for p in &mut bezier.points {
|
||||||
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 {
|
|
||||||
*p = transform * *p;
|
*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) => {
|
Self::Callback(shape) => {
|
||||||
shape.rect = transform * shape.rect;
|
shape.rect = transform * shape.rect;
|
||||||
|
|
@ -502,7 +503,7 @@ fn points_from_line(
|
||||||
shapes: &mut Vec<Shape>,
|
shapes: &mut Vec<Shape>,
|
||||||
) {
|
) {
|
||||||
let mut position_on_segment = 0.0;
|
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 (start, end) = (window[0], window[1]);
|
||||||
let vector = end - start;
|
let vector = end - start;
|
||||||
let segment_length = vector.length();
|
let segment_length = vector.length();
|
||||||
|
|
@ -512,7 +513,7 @@ fn points_from_line(
|
||||||
position_on_segment += spacing;
|
position_on_segment += spacing;
|
||||||
}
|
}
|
||||||
position_on_segment -= segment_length;
|
position_on_segment -= segment_length;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates dashes from a line.
|
/// Creates dashes from a line.
|
||||||
|
|
@ -529,7 +530,7 @@ fn dashes_from_line(
|
||||||
let mut drawing_dash = false;
|
let mut drawing_dash = false;
|
||||||
let mut step = 0;
|
let mut step = 0;
|
||||||
let steps = dash_lengths.len();
|
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 (start, end) = (window[0], window[1]);
|
||||||
let vector = end - start;
|
let vector = end - start;
|
||||||
let segment_length = vector.length();
|
let segment_length = vector.length();
|
||||||
|
|
@ -560,5 +561,5 @@ fn dashes_from_line(
|
||||||
}
|
}
|
||||||
|
|
||||||
position_on_segment -= segment_length;
|
position_on_segment -= segment_length;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
/// Set the stroke to be painted right on the edge of the shape, half inside and half outside.
|
||||||
|
#[inline]
|
||||||
pub fn middle(self) -> Self {
|
pub fn middle(self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
kind: StrokeKind::Middle,
|
kind: StrokeKind::Middle,
|
||||||
|
|
@ -128,6 +134,7 @@ impl PathStroke {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the stroke to be painted entirely outside of the shape
|
/// Set the stroke to be painted entirely outside of the shape
|
||||||
|
#[inline]
|
||||||
pub fn outside(self) -> Self {
|
pub fn outside(self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
kind: StrokeKind::Outside,
|
kind: StrokeKind::Outside,
|
||||||
|
|
@ -136,6 +143,7 @@ impl PathStroke {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the stroke to be painted entirely inside of the shape
|
/// Set the stroke to be painted entirely inside of the shape
|
||||||
|
#[inline]
|
||||||
pub fn inside(self) -> Self {
|
pub fn inside(self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
kind: StrokeKind::Inside,
|
kind: StrokeKind::Inside,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use emath::{pos2, remap, vec2, GuiRounding as _, NumExt, Pos2, Rect, Rot2, Vec2}
|
||||||
use crate::{
|
use crate::{
|
||||||
color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, CircleShape,
|
color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, CircleShape,
|
||||||
ClippedPrimitive, ClippedShape, Color32, CubicBezierShape, EllipseShape, Mesh, PathShape,
|
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,
|
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.
|
/// Open-ended.
|
||||||
pub fn stroke_open(&mut self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) {
|
pub fn stroke_open(&mut self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) {
|
||||||
stroke_path(feathering, &mut self.0, PathType::Open, stroke, out);
|
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).
|
/// 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.
|
/// Calling this may reverse the vertices in the path if they are wrong winding order.
|
||||||
///
|
|
||||||
/// The preferred winding order is clockwise.
|
/// The preferred winding order is clockwise.
|
||||||
///
|
pub fn fill(&mut self, feathering: f32, color: Color32, out: &mut Mesh) {
|
||||||
/// The stroke colors is used for color-correct feathering.
|
fill_closed_path(feathering, &mut self.0, color, out);
|
||||||
pub fn fill(&mut self, feathering: f32, color: Color32, stroke: &PathStroke, out: &mut Mesh) {
|
|
||||||
fill_closed_path(feathering, &mut self.0, color, stroke, out);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like [`Self::fill`] but with texturing.
|
/// Like [`Self::fill`] but with texturing.
|
||||||
|
|
@ -523,11 +534,11 @@ impl Path {
|
||||||
|
|
||||||
pub mod path {
|
pub mod path {
|
||||||
//! Helpers for constructing paths
|
//! Helpers for constructing paths
|
||||||
use crate::Rounding;
|
use crate::Roundingf;
|
||||||
use emath::{pos2, Pos2, Rect};
|
use emath::{pos2, Pos2, Rect};
|
||||||
|
|
||||||
/// overwrites existing points
|
/// 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();
|
path.clear();
|
||||||
|
|
||||||
let min = rect.min;
|
let min = rect.min;
|
||||||
|
|
@ -535,7 +546,7 @@ pub mod path {
|
||||||
|
|
||||||
let r = clamp_rounding(rounding, rect);
|
let r = clamp_rounding(rounding, rect);
|
||||||
|
|
||||||
if r == Rounding::ZERO {
|
if r == Roundingf::ZERO {
|
||||||
path.reserve(4);
|
path.reserve(4);
|
||||||
path.push(pos2(min.x, min.y)); // left top
|
path.push(pos2(min.x, min.y)); // left top
|
||||||
path.push(pos2(max.x, min.y)); // right 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.
|
// Duplicated vertices can happen when one side is all rounding, with no straight edge between.
|
||||||
let eps = f32::EPSILON * rect.size().max_elem();
|
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
|
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 {
|
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
|
// 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_width = rect.width() * 0.5;
|
||||||
let half_height = rect.height() * 0.5;
|
let half_height = rect.height() * 0.5;
|
||||||
let max_cr = half_width.min(half_height);
|
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.
|
/// Calling this may reverse the vertices in the path if they are wrong winding order.
|
||||||
///
|
///
|
||||||
/// The preferred winding order is clockwise.
|
/// The preferred winding order is clockwise.
|
||||||
///
|
fn fill_closed_path(feathering: f32, path: &mut [PathPoint], fill_color: Color32, out: &mut Mesh) {
|
||||||
/// A stroke is required so that the fill's feathering can fade to the right color. You can pass `&PathStroke::NONE` if
|
if fill_color == Color32::TRANSPARENT {
|
||||||
/// 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 {
|
|
||||||
return;
|
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;
|
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 {
|
if cw_signed_area(path) < 0.0 {
|
||||||
// Wrong winding order - fix:
|
// Wrong winding order - fix:
|
||||||
path.reverse();
|
path.reverse();
|
||||||
|
|
@ -809,10 +799,9 @@ fn fill_closed_path(
|
||||||
|
|
||||||
let pos_inner = p1.pos - dm;
|
let pos_inner = p1.pos - dm;
|
||||||
let pos_outer = 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_inner, fill_color);
|
||||||
out.colored_vertex(pos_outer, color_outer);
|
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_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);
|
out.add_triangle(idx_outer + i0 * 2, idx_outer + i1 * 2, idx_inner + 2 * i1);
|
||||||
i0 = i1;
|
i0 = i1;
|
||||||
|
|
@ -823,7 +812,7 @@ fn fill_closed_path(
|
||||||
out.vertices.extend(path.iter().map(|p| Vertex {
|
out.vertices.extend(path.iter().map(|p| Vertex {
|
||||||
pos: p.pos,
|
pos: p.pos,
|
||||||
uv: WHITE_UV,
|
uv: WHITE_UV,
|
||||||
color,
|
color: fill_color,
|
||||||
}));
|
}));
|
||||||
for i in 2..n {
|
for i in 2..n {
|
||||||
out.add_triangle(idx, idx + i - 1, idx + i);
|
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;
|
let n = path.len() as u32;
|
||||||
if feathering > 0.0 {
|
if 0.0 < feathering {
|
||||||
if cw_signed_area(path) < 0.0 {
|
if cw_signed_area(path) < 0.0 {
|
||||||
// Wrong winding order - fix:
|
// Wrong winding order - fix:
|
||||||
path.reverse();
|
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.
|
/// Tessellate the given path as a stroke with thickness.
|
||||||
fn stroke_path(
|
fn stroke_path(
|
||||||
feathering: f32,
|
feathering: f32,
|
||||||
|
|
@ -935,55 +910,122 @@ fn stroke_path(
|
||||||
path_type: PathType,
|
path_type: PathType,
|
||||||
stroke: &PathStroke,
|
stroke: &PathStroke,
|
||||||
out: &mut Mesh,
|
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;
|
let n = path.len() as u32;
|
||||||
|
|
||||||
if stroke.is_empty() || n < 2 {
|
if n < 2 {
|
||||||
return;
|
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;
|
let idx = out.vertices.len() as u32;
|
||||||
|
|
||||||
// Translate the points along their normals if the stroke is outside or inside
|
// Move the points so that the stroke is on middle of the path.
|
||||||
if stroke.kind != StrokeKind::Middle {
|
match stroke.kind {
|
||||||
path.iter_mut()
|
StrokeKind::Inside => {
|
||||||
.for_each(|p| translate_stroke_point(p, stroke));
|
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
|
// 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>>())
|
Rect::from_points(&path.iter().map(|p| p.pos).collect::<Vec<Pos2>>())
|
||||||
.expand((stroke.width / 2.0) + feathering)
|
.expand((stroke.width / 2.0) + feathering)
|
||||||
} else {
|
} else {
|
||||||
Rect::NAN
|
Rect::NAN
|
||||||
};
|
};
|
||||||
|
|
||||||
let get_color = |col: &ColorMode, pos: Pos2| match col {
|
let get_color = |col: &ColorMode, pos: Pos2| match col {
|
||||||
ColorMode::Solid(col) => *col,
|
ColorMode::Solid(col) => *col,
|
||||||
ColorMode::UV(fun) => fun(bbox, pos),
|
ColorMode::UV(fun) => fun(uv_bbox, pos),
|
||||||
};
|
};
|
||||||
|
|
||||||
if feathering > 0.0 {
|
if 0.0 < feathering {
|
||||||
let color_inner = &stroke.color;
|
|
||||||
let color_outer = Color32::TRANSPARENT;
|
let color_outer = Color32::TRANSPARENT;
|
||||||
|
let color_middle = &stroke.color;
|
||||||
|
|
||||||
let thin_line = stroke.width <= feathering;
|
let thin_line = stroke.width <= feathering;
|
||||||
if thin_line {
|
if thin_line {
|
||||||
/*
|
// If the stroke is painted smaller than the pixel width (=feathering width),
|
||||||
We paint the line using three edges: outer, inner, outer.
|
// then we risk severe aliasing.
|
||||||
|
// Instead, we paint the stroke as a triangular ridge, two feather-widths wide,
|
||||||
. o i o outer, inner, outer
|
// and lessen the opacity of the middle part instead of making it thinner.
|
||||||
. |---| feathering (pixel width)
|
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
|
||||||
// Fade out as it gets thinner:
|
// artificially wide line.
|
||||||
if let ColorMode::Solid(col) = color_inner {
|
for point in &mut *path {
|
||||||
let color_inner = mul_color(*col, stroke.width / feathering);
|
point.pos += 0.5 * (feathering - stroke.width) * point.normal;
|
||||||
if color_inner == Color32::TRANSPARENT {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_triangles(4 * n as usize);
|
||||||
out.reserve_vertices(3 * n as usize);
|
out.reserve_vertices(3 * n as usize);
|
||||||
|
|
||||||
|
|
@ -994,11 +1036,8 @@ fn stroke_path(
|
||||||
let p = p1.pos;
|
let p = p1.pos;
|
||||||
let n = p1.normal;
|
let n = p1.normal;
|
||||||
out.colored_vertex(p + n * feathering, color_outer);
|
out.colored_vertex(p + n * feathering, color_outer);
|
||||||
out.colored_vertex(
|
out.colored_vertex(p, mul_color(get_color(color_middle, p), opacity));
|
||||||
p,
|
out.colored_vertex(p - n * feathering, color_fill);
|
||||||
mul_color(get_color(color_inner, p), stroke.width / feathering),
|
|
||||||
);
|
|
||||||
out.colored_vertex(p - n * feathering, color_outer);
|
|
||||||
|
|
||||||
if connect_with_previous {
|
if connect_with_previous {
|
||||||
out.add_triangle(idx + 3 * i0 + 0, idx + 3 * i0 + 1, idx + 3 * i1 + 0);
|
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 + 1, idx + 3 * i0 + 2, idx + 3 * i1 + 1);
|
||||||
out.add_triangle(idx + 3 * i0 + 2, idx + 3 * i1 + 1, idx + 3 * i1 + 2);
|
out.add_triangle(idx + 3 * i0 + 2, idx + 3 * i1 + 1, idx + 3 * i1 + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
i0 = i1;
|
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 {
|
} else {
|
||||||
// thick anti-aliased line
|
// 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)
|
. |---| feathering (pixel width)
|
||||||
. |--------------| width
|
. |--------------| width
|
||||||
. |---------| outer_rad
|
. |---------| outer_rad
|
||||||
|
|
@ -1038,13 +1086,13 @@ fn stroke_path(
|
||||||
out.colored_vertex(p + n * outer_rad, color_outer);
|
out.colored_vertex(p + n * outer_rad, color_outer);
|
||||||
out.colored_vertex(
|
out.colored_vertex(
|
||||||
p + n * inner_rad,
|
p + n * inner_rad,
|
||||||
get_color(color_inner, p + n * inner_rad),
|
get_color(color_middle, p + n * inner_rad),
|
||||||
);
|
);
|
||||||
out.colored_vertex(
|
out.colored_vertex(
|
||||||
p - n * inner_rad,
|
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 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0);
|
||||||
out.add_triangle(idx + 4 * i0 + 1, idx + 4 * i1 + 0, idx + 4 * i1 + 1);
|
out.add_triangle(idx + 4 * i0 + 1, idx + 4 * i1 + 0, idx + 4 * i1 + 1);
|
||||||
|
|
@ -1057,6 +1105,14 @@ fn stroke_path(
|
||||||
|
|
||||||
i0 = i1;
|
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 => {
|
PathType::Open => {
|
||||||
// Anti-alias the ends by extruding the outer edge and adding
|
// 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 * outer_rad + back_extrude, color_outer);
|
||||||
out.colored_vertex(
|
out.colored_vertex(
|
||||||
p + n * inner_rad,
|
p + n * inner_rad,
|
||||||
get_color(color_inner, p + n * inner_rad),
|
get_color(color_middle, p + n * inner_rad),
|
||||||
);
|
);
|
||||||
out.colored_vertex(
|
out.colored_vertex(
|
||||||
p - n * inner_rad,
|
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);
|
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 * outer_rad, color_outer);
|
||||||
out.colored_vertex(
|
out.colored_vertex(
|
||||||
p + n * inner_rad,
|
p + n * inner_rad,
|
||||||
get_color(color_inner, p + n * inner_rad),
|
get_color(color_middle, p + n * inner_rad),
|
||||||
);
|
);
|
||||||
out.colored_vertex(
|
out.colored_vertex(
|
||||||
p - n * inner_rad,
|
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_outer);
|
||||||
|
|
||||||
|
|
@ -1133,11 +1189,11 @@ fn stroke_path(
|
||||||
out.colored_vertex(p + n * outer_rad + back_extrude, color_outer);
|
out.colored_vertex(p + n * outer_rad + back_extrude, color_outer);
|
||||||
out.colored_vertex(
|
out.colored_vertex(
|
||||||
p + n * inner_rad,
|
p + n * inner_rad,
|
||||||
get_color(color_inner, p + n * inner_rad),
|
get_color(color_middle, p + n * inner_rad),
|
||||||
);
|
);
|
||||||
out.colored_vertex(
|
out.colored_vertex(
|
||||||
p - n * inner_rad,
|
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);
|
out.colored_vertex(p - n * outer_rad + back_extrude, color_outer);
|
||||||
|
|
||||||
|
|
@ -1183,32 +1239,21 @@ fn stroke_path(
|
||||||
let thin_line = stroke.width <= feathering;
|
let thin_line = stroke.width <= feathering;
|
||||||
if thin_line {
|
if thin_line {
|
||||||
// Fade out thin lines rather than making them thinner
|
// Fade out thin lines rather than making them thinner
|
||||||
|
let opacity = stroke.width / feathering;
|
||||||
let radius = feathering / 2.0;
|
let radius = feathering / 2.0;
|
||||||
if let ColorMode::Solid(color) = stroke.color {
|
for p in path.iter_mut() {
|
||||||
let color = mul_color(color, stroke.width / feathering);
|
|
||||||
if color == Color32::TRANSPARENT {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for p in path {
|
|
||||||
out.colored_vertex(
|
out.colored_vertex(
|
||||||
p.pos + radius * p.normal,
|
p.pos + radius * p.normal,
|
||||||
mul_color(
|
mul_color(get_color(&stroke.color, p.pos + radius * p.normal), opacity),
|
||||||
get_color(&stroke.color, p.pos + radius * p.normal),
|
|
||||||
stroke.width / feathering,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
out.colored_vertex(
|
out.colored_vertex(
|
||||||
p.pos - radius * p.normal,
|
p.pos - radius * p.normal,
|
||||||
mul_color(
|
mul_color(get_color(&stroke.color, p.pos - radius * p.normal), opacity),
|
||||||
get_color(&stroke.color, p.pos - radius * p.normal),
|
|
||||||
stroke.width / feathering,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let radius = stroke.width / 2.0;
|
let radius = stroke.width / 2.0;
|
||||||
for p in path {
|
for p in path.iter_mut() {
|
||||||
out.colored_vertex(
|
out.colored_vertex(
|
||||||
p.pos + radius * p.normal,
|
p.pos + radius * p.normal,
|
||||||
get_color(&stroke.color, 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.clear();
|
||||||
self.scratchpad_path.add_circle(center, radius);
|
self.scratchpad_path.add_circle(center, radius);
|
||||||
self.scratchpad_path
|
self.scratchpad_path
|
||||||
.fill(self.feathering, fill, &path_stroke, out);
|
.fill_and_stroke(self.feathering, fill, &path_stroke, out);
|
||||||
self.scratchpad_path
|
|
||||||
.stroke_closed(self.feathering, &path_stroke, out);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tessellate a single [`EllipseShape`] into a [`Mesh`].
|
/// Tessellate a single [`EllipseShape`] into a [`Mesh`].
|
||||||
|
|
@ -1536,9 +1591,7 @@ impl Tessellator {
|
||||||
self.scratchpad_path.clear();
|
self.scratchpad_path.clear();
|
||||||
self.scratchpad_path.add_line_loop(&points);
|
self.scratchpad_path.add_line_loop(&points);
|
||||||
self.scratchpad_path
|
self.scratchpad_path
|
||||||
.fill(self.feathering, fill, &path_stroke, out);
|
.fill_and_stroke(self.feathering, fill, &path_stroke, out);
|
||||||
self.scratchpad_path
|
|
||||||
.stroke_closed(self.feathering, &path_stroke, out);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tessellate a single [`Mesh`] into a [`Mesh`].
|
/// Tessellate a single [`Mesh`] into a [`Mesh`].
|
||||||
|
|
@ -1642,27 +1695,24 @@ impl Tessellator {
|
||||||
} = path_shape;
|
} = path_shape;
|
||||||
|
|
||||||
self.scratchpad_path.clear();
|
self.scratchpad_path.clear();
|
||||||
|
|
||||||
if *closed {
|
if *closed {
|
||||||
self.scratchpad_path.add_line_loop(points);
|
self.scratchpad_path.add_line_loop(points);
|
||||||
} else {
|
|
||||||
self.scratchpad_path.add_open_points(points);
|
|
||||||
}
|
|
||||||
|
|
||||||
if *fill != Color32::TRANSPARENT {
|
self.scratchpad_path
|
||||||
debug_assert!(
|
.fill_and_stroke(self.feathering, *fill, stroke, out);
|
||||||
closed,
|
} else {
|
||||||
|
debug_assert_eq!(
|
||||||
|
*fill,
|
||||||
|
Color32::TRANSPARENT,
|
||||||
"You asked to fill a path that is not closed. That makes no sense."
|
"You asked to fill a path that is not closed. That makes no sense."
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.scratchpad_path.add_open_points(points);
|
||||||
|
|
||||||
self.scratchpad_path
|
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`].
|
/// Tessellate a single [`Rect`] into a [`Mesh`].
|
||||||
|
|
@ -1679,18 +1729,69 @@ impl Tessellator {
|
||||||
let brush = rect_shape.brush.as_ref();
|
let brush = rect_shape.brush.as_ref();
|
||||||
let RectShape {
|
let RectShape {
|
||||||
mut rect,
|
mut rect,
|
||||||
mut rounding,
|
rounding,
|
||||||
fill,
|
mut fill,
|
||||||
mut stroke,
|
mut stroke,
|
||||||
stroke_kind,
|
mut stroke_kind,
|
||||||
round_to_pixels,
|
round_to_pixels,
|
||||||
mut blur_width,
|
mut blur_width,
|
||||||
brush: _, // brush is extracted on its own, because it is not Copy
|
brush: _, // brush is extracted on its own, because it is not Copy
|
||||||
} = *rect_shape;
|
} = *rect_shape;
|
||||||
|
|
||||||
|
let mut rounding = Roundingf::from(rounding);
|
||||||
let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels);
|
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 {
|
if round_to_pixels {
|
||||||
// The rounding is aware of the stroke kind.
|
// The rounding is aware of the stroke kind.
|
||||||
// It is designed to be clever in trying to divine the intentions of the user.
|
// 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.
|
// On this path we optimize for crisp and symmetric strokes.
|
||||||
// We put odd-width strokes in the center of pixels.
|
// We put odd-width strokes in the center of pixels.
|
||||||
// To understand why, see `fn round_line_segment`.
|
// 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)
|
|| is_nearest_integer_odd(self.pixels_per_point * stroke.width)
|
||||||
{
|
{
|
||||||
rect = rect.round_to_pixel_center(self.pixels_per_point);
|
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;
|
let old_feathering = self.feathering;
|
||||||
|
|
||||||
if self.feathering < blur_width {
|
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.
|
// 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.
|
// 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,
|
let eps = 0.1; // avoid numerical problems
|
||||||
// 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;
|
|
||||||
blur_width = blur_width
|
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);
|
.at_least(0.0);
|
||||||
|
|
||||||
rounding += Rounding::from(0.5 * blur_width);
|
rounding += 0.5 * blur_width;
|
||||||
|
|
||||||
self.feathering = self.feathering.max(blur_width);
|
self.feathering = self.feathering.max(blur_width);
|
||||||
}
|
}
|
||||||
|
|
||||||
if rect.width() < 0.5 * self.feathering {
|
{
|
||||||
// Very thin - approximate by a vertical line-segment:
|
// Modify `rect` so that it represents the OUTER border
|
||||||
// There is room for improvement here, but it is not critical.
|
// We do this because `path::rounded_rectangle` uses the
|
||||||
let line = [rect.center_top(), rect.center_bottom()];
|
// corner radius to pick the fidelity/resolution of the corner.
|
||||||
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();
|
|
||||||
|
|
||||||
if rect.is_positive() {
|
let original_rounding = rounding;
|
||||||
// Fill
|
|
||||||
if let Some(brush) = brush {
|
match stroke_kind {
|
||||||
// Textured
|
StrokeKind::Inside => {}
|
||||||
let crate::Brush {
|
StrokeKind::Middle => {
|
||||||
fill_texture_id,
|
rect = rect.expand(stroke.width / 2.0);
|
||||||
uv,
|
rounding += stroke.width / 2.0;
|
||||||
} = **brush;
|
}
|
||||||
let uv_from_pos = |p: Pos2| {
|
StrokeKind::Outside => {
|
||||||
pos2(
|
rect = rect.expand(stroke.width);
|
||||||
remap(p.x, rect.x_range(), uv.x_range()),
|
rounding += stroke.width;
|
||||||
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);
|
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
|
self.feathering = old_feathering; // restore
|
||||||
|
|
@ -2029,24 +2139,21 @@ impl Tessellator {
|
||||||
self.scratchpad_path.clear();
|
self.scratchpad_path.clear();
|
||||||
if closed {
|
if closed {
|
||||||
self.scratchpad_path.add_line_loop(points);
|
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
|
self.scratchpad_path
|
||||||
.fill(self.feathering, fill, stroke, out);
|
.fill_and_stroke(self.feathering, fill, stroke, out);
|
||||||
}
|
|
||||||
let typ = if closed {
|
|
||||||
PathType::Closed
|
|
||||||
} else {
|
} else {
|
||||||
PathType::Open
|
debug_assert_eq!(
|
||||||
};
|
fill,
|
||||||
self.scratchpad_path
|
Color32::TRANSPARENT,
|
||||||
.stroke(self.feathering, typ, stroke, out);
|
"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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue