mirror of
https://github.com/emilk/egui.git
synced 2026-08-31 13:50:04 -04:00
Improve tessellation quality (#5669)
## Defining what `Rounding` is This PR defines what `Rounding` means: it is the corner radius of underlying `RectShape` rectangle. If you use `StrokeKind::Inside`, this means the rounding is of the outer part of the stroke. Conversely, if you use `StrokeKind::Outside`, the stroke is outside the rounded rectangle, so the stroke has an inner radius or `rounding`, and an outer radius that is larger by `stroke.width`. This definitions is the same as Figma uses. ## Improving general shape rendering The rendering of filled shapes (rectangles, circles, paths, bezier) has been rewritten. Instead of first painting the fill with the stroke on top, we now paint them as one single mesh with shared vertices at the border. This has several benefits: * Less work (faster and with fewer vertices produced) * No overdraw (nicer rendering of translucent shapes) * Correct blending of stroke and fill The logic for rendering thin strokes has also been improved, so that the width of a stroke of `StrokeKind::Outside` never affects the filled area (this used to be wrong for thin strokes). ## Improving of rectangle rendering Rectangles also has specific improvements in how thin rectangles are painted. The handling of "Blur width" is also a lot better, and now works for rectangles with strokes. There also used to be bugs with specific combinations of corner radius and stroke width, that are now fixed. ## But why? With the new `egui::Scene` we end up with a lot of zoomed out shapes, with sub-pixel strokes. These need to look good! One thing led to another, and then I became obsessive 😅 ## Tessellation Test In order to investigate the rendering, I created a Tessellation Test in the `egui_demo_lib`. [Try it here](https://egui-pr-preview.github.io/pr/5669-emilkimprove-tessellator)  
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user