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 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]),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2292f0f80bfd3c80055a72eb983549ac2875d36acb333732bd0a67e51b24ae4f
|
||||
size 102983
|
||||
oid sha256:57274cec5ee7e5522073249b931ea65ead22752aea1de40666543e765c1b6b85
|
||||
size 102929
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
]),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
oid sha256:d4cfd5191dc7046a782ef2350dc8e0547d2702182badcb15b6b928ce077b76c1
|
||||
size 32154
|
||||
oid sha256:23f19871720b67659a7b56cee8a78edc941c4bac86f55efc6fa549f10c4712fb
|
||||
size 31754
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4631f841b9e23833505af39eb1c45908013d3b1e1278d477bcedf6a460c71802
|
||||
size 27163
|
||||
oid sha256:e4d8fee9fd8e69ecd60ebd0dd41c29b61cc13e7013b1d20ad93d40fc4ed1cc03
|
||||
size 27091
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:96e750ebfcc6ec2c130674407388c30cce844977cde19adfebf351fd08698a4f
|
||||
size 81726
|
||||
oid sha256:b291e7efd895ab095590285b841903f05dc7d4dadbab7d9001b04a92953c1694
|
||||
size 81677
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b03613e597631da3b922ef3d696c4ce74cec41f86a2779fc5b084a316fc9e8e8
|
||||
size 11764
|
||||
oid sha256:5d75230689807ae7fb692bac5c9b33ee04c02b9e54963e2d6ada05860157daf6
|
||||
size 11705
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:76d77e2df39af248d004a023b238bb61ed598ab2bea3e0c6f2885f9537ec1103
|
||||
size 25988
|
||||
oid sha256:f512159108c7681834ae2a52c5e1d4eb4dbe678a509f3dab3384e35d6c8dbee2
|
||||
size 25865
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:003d905893b80ffc132a783155971ad3054229e9d6c046e2760c912559a66b3d
|
||||
size 20869
|
||||
oid sha256:f2a4f5e67ff6615877794304e8be5a5db647d48e20e4248259179cddefbf4088
|
||||
size 20806
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:be0f96c700b7662aab5098f8412dae3676116eeed65e70f6b295dd3375b329d0
|
||||
size 10968
|
||||
oid sha256:0bc474a37d6d3a7a08dd41963bf9009c05315de91bb515cc2b19443b79480bff
|
||||
size 10723
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:99c94a09c0f6984909481189f9a1415ea90bd7c45e42b4b3763ee36f3d831a65
|
||||
size 133231
|
||||
oid sha256:0bff769b3f9e46c5e38170885b2c8301294cbcc1c8ee22c17672205edd509924
|
||||
size 133170
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e4fef5fa8661f207bae2c58381e729cdaf77aecc8b3f178caf262dc310e3a490
|
||||
size 24206
|
||||
oid sha256:f90f94a842a1d0f1386c3cdd28e60e6ad6efe968f510a11bde418b5bc70a81d2
|
||||
size 23897
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7b38828e423195628dcea09b0cbdd66aa4c077334ab7082efd358c7a3297009d
|
||||
size 17827
|
||||
oid sha256:fdbdf1159f0ab4579bf9ceefbd8e40ac7af468368ab11ffec8578214b57d867c
|
||||
size 17758
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c53403996451da14f7991ea61bd20b96dbc67eb67dd2041975dd6ce5015a6980
|
||||
size 22485
|
||||
oid sha256:ce647fd9626f126e7f19b4494bb98a2086071f9c45f7dd6ba4708632e7433a7b
|
||||
size 22418
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:63673b070951246b98ca07613fa81496dbfdd10627bac3c9c4356ebff1a36b20
|
||||
size 64319
|
||||
oid sha256:a843e6772809a1c904968fac19b49973675e726a3b7727ca1b898ad3b9072b0c
|
||||
size 64257
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:374b4095a3c7b827b80d6ab01b945458ae0128a2974c2af8aaf6b7e9fef6b459
|
||||
size 32554
|
||||
oid sha256:b437f27c46ddf82e4268d9bb86d33f43c382d1ab3ed45297bc136600cdc9960c
|
||||
size 32493
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e756c90069803bb4d2821fff723877bffffd44b26613f5b06c8a042d6901ca4
|
||||
size 36578
|
||||
oid sha256:2cd0bda8b4d3d7f833273097c8bb52cfd8ea63a405d6798b9aeb7002b143ac74
|
||||
size 36459
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6efc59cb9908533baa1a7346b359e9e21c5faf0e373dac6fa7db5476e644233d
|
||||
size 17678
|
||||
oid sha256:a57c3bf373a283b79188080e2ddf7407c2974655fc5ad59222442e14faae055c
|
||||
size 17508
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eb631bd2608aec6c83f8815b9db0b28627bf82692fd2b1cb793119083b4f8ad1
|
||||
size 264496
|
||||
oid sha256:161b5853f528206f2531587753369f2f6f3c52203af668eba0d81a41c2a915e0
|
||||
size 264432
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9fee66bab841602862c301a7df3add365ea93fde0ac9b71ca7f9b74a709a68d2
|
||||
size 35576
|
||||
oid sha256:118413a5ec56c6589914c5cb59bda2884a4d6acec3b34bbc367bc893c3de8127
|
||||
size 35409
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e0870e9e1c9dc718690124a4d490f1061414f15fa40571e571d9319c3b65e74e
|
||||
size 23709
|
||||
oid sha256:361791f30739f841c46433f5d16d281ba9d0c52027b4cf156c3ac293aa795f46
|
||||
size 23592
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:532dbcbec94bb9c9fb8cc0665646790a459088e98077118b5fbb2112898e1a43
|
||||
size 183854
|
||||
oid sha256:81338d7b0412989590bc0808419d93788e972e7ab6f70f481f86bd23263a5395
|
||||
size 183821
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3fd584643b68084ec4b65369e08d229e2d389551bbefa59c84ad4b58621593f7
|
||||
size 117754
|
||||
oid sha256:41c51350c09360738ca284b05c944a11164e4a614beb95ce4cc962222af33d87
|
||||
size 117764
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e2b854d99c9b17d15ef92188cdac521d7c0635aef9ba457cd3801884a784009
|
||||
size 26159
|
||||
oid sha256:794475478cfe2fc954731742165b1f0419a0e64616e72b3766c1e031dbba7ca1
|
||||
size 26092
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f0b7dc029de8e669d76f3be5e0800e973c354edcf7cefa239faed07c2cd5e0d5
|
||||
size 70452
|
||||
oid sha256:7073aa30f3e7dfd116d9a4f02f6c5a075ce068d2e6f43eaede3c4dd6c56f925e
|
||||
size 70439
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:64ba40810a6480e511e8d198b0dfb39e8b220eb2f5592877e27b17ee8d47b9c3
|
||||
size 66387
|
||||
oid sha256:4bb3cd05f253c0e109b83a0556b975af7a96d57678e57de3b9fa130cc8a8de1c
|
||||
size 66318
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0a8151f5bd01b2cb5183c532d11f57bbb7e8cc1e77a3c4b890537d157922c961
|
||||
size 21261
|
||||
oid sha256:5b550769d5ec5d834f89fba56375d10e5f9b3ba703599d90a5b6607aff1c2b06
|
||||
size 21194
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:96ce36fcc334793801ab724c711f592faf086a9c98c812684e6b76010e9d1974
|
||||
size 59714
|
||||
oid sha256:45f343b55be98976de32bd3c5438212a46b787123ea3096cf8fc6bec99493184
|
||||
size 59699
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8155d93b78692ced67ddee4011526ef627838ede78351871df5feef8aa512857
|
||||
size 13141
|
||||
oid sha256:8d25f774320ca844fa4dfba5215ed66f067d0f30c6d7c8ad7ec29d97ee7732e0
|
||||
size 13073
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bf6da022ab97f9d4b862cc8e91bdfd7f9785a3ab0540aa1c2bd2646bd30a3450
|
||||
size 35115
|
||||
oid sha256:540a05c5b1de7e362bcb48a04611323fbc65c8787b8ae852ada8aa145803753d
|
||||
size 34968
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:035b35ed053cabd5022d9a97f7d4d5a79d194c9f14594e7602ecd9ef94c24ae5
|
||||
size 48053
|
||||
oid sha256:2276b8221389da4f644cad43ce446cd7d28f41820cea36fa3ea860808710053d
|
||||
size 47878
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:80a2968e211c83639b60e84b805f1327fb37b87144cada672a403c7e92ace8a8
|
||||
size 48066
|
||||
oid sha256:054d606681e08bf61762e5c7d7596c4f483e66ac889795e4be4956103328e0bb
|
||||
size 47862
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:86df5dc4b4ddd6f44226242b6d9b5e9f2aacd45193ae9f784fb5084a7a509e0b
|
||||
size 43987
|
||||
oid sha256:f7fb29285e53b619f333757052d05f86d973680ac1ce6d1b25d28b7824bbce51
|
||||
size 43725
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1138bbc3b7e73cccd555f0fd58c27a5bda4d84484fdc1bd5223fc9802d0c5328
|
||||
size 44089
|
||||
oid sha256:be50a396838d4fe29bcfd0807377ad0cda538fea8569acf709da0b13505bf09b
|
||||
size 43871
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3562bb87b155b2c406a72a651ffb1991305aa1e15273ce9f32cedc5766318537
|
||||
size 554922
|
||||
oid sha256:9c6ce16a8a8c34d882485da6bbe08039fb55f90636da8136f68b1bb9baf0effb
|
||||
size 557610
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ba6c0bd127ab02375f2ae5fbc3eeef33a1bdf074cbb180de2733c700b81df3e5
|
||||
size 771069
|
||||
oid sha256:a49c052578a46adb41bc02c6d7fdc264ed0ab8ae636cc8a11ac729fe1e48091b
|
||||
size 791802
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d85ab6d04059009fd2c3ad8001332b27e710c46c9300f2f1f409b882c49211dc
|
||||
size 918967
|
||||
oid sha256:989c55a83b8bc7cce4f459d8b835962377927a98f2bce085e92cba8438438ecd
|
||||
size 943736
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a64f1bdec565357fe4dee3acb46b12eeb0492b522fb3bb9697d39dadce2e8c21
|
||||
size 1039455
|
||||
oid sha256:01ac61dc5bcecf6bf0d13c8399460b1afae652efe6fecc1d0e4b2f27d9f1c5a4
|
||||
size 1046906
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5ca008dca03372bb334564e55fa2d1d25a36751a43df6001a1c1cf3e4db9bcd4
|
||||
size 1130930
|
||||
oid sha256:3348923ecbad34e385e3ed52ab9b7c88b5d4fc07de00620302d5b191d90a453f
|
||||
size 1140236
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7f695127e7fe6cb3b752a0acd71db85d51d2de68e45051a7afe91f4d960acf27
|
||||
size 1311641
|
||||
oid sha256:179c17b0405c6e87bd3cafaa7272e3e6d6eefd462b4406cca2a7abfe8af6f2bd
|
||||
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
|
||||
oid sha256:a291f3a5724aefc59ba7881f48752ccc826ca5e480741c221d195061f562ccc9
|
||||
size 158220
|
||||
oid sha256:946bf96ae558ee7373b50bf11959e82b1f4d91866ec61b04b0336ae170b6f7b2
|
||||
size 158553
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:64fd46da67cab2afae0ea8997a88fb43fd207e24cc3943086d978a8de717320f
|
||||
size 7542
|
||||
oid sha256:f65efbf60e190d83d187ec51f3f7811eb55135ef4feb9586e931e8498bc05d64
|
||||
size 7430
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue