[tesselator] hide Path from view and improve thin rounded rectangles

This commit is contained in:
Emil Ernerfeldt 2020-09-01 20:03:50 +02:00
parent 96153a86e5
commit 62b1a2658f
7 changed files with 86 additions and 79 deletions

View file

@ -2,7 +2,7 @@ use std::hash::Hash;
use crate::{ use crate::{
layout::Direction, layout::Direction,
paint::{LineStyle, PaintCmd, Path, TextStyle}, paint::{LineStyle, PaintCmd, TextStyle},
widgets::Label, widgets::Label,
*, *,
}; };
@ -112,7 +112,7 @@ pub fn paint_icon(ui: &mut Ui, openness: f32, response: &Response) {
// Draw a pointy triangle arrow: // Draw a pointy triangle arrow:
let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75); let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75);
let mut points = [rect.left_top(), rect.right_top(), rect.center_bottom()]; let mut points = vec![rect.left_top(), rect.right_top(), rect.center_bottom()];
let rotation = Vec2::angled(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0)); let rotation = Vec2::angled(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
for p in &mut points { for p in &mut points {
let v = *p - rect.center(); let v = *p - rect.center();
@ -121,7 +121,7 @@ pub fn paint_icon(ui: &mut Ui, openness: f32, response: &Response) {
} }
ui.painter().add(PaintCmd::Path { ui.painter().add(PaintCmd::Path {
path: Path::from_point_loop(&points), points,
closed: true, closed: true,
fill: Default::default(), fill: Default::default(),
outline: LineStyle::new(stroke_width, stroke_color), outline: LineStyle::new(stroke_width, stroke_color),

View file

@ -531,39 +531,47 @@ fn paint_frame_interaction(
interaction: WindowInteraction, interaction: WindowInteraction,
visuals: style::WidgetVisuals, visuals: style::WidgetVisuals,
) { ) {
use paint::tessellator::path::add_circle_quadrant;
let cr = ui.style().visuals.window_corner_radius; let cr = ui.style().visuals.window_corner_radius;
let Rect { min, max } = rect; let Rect { min, max } = rect;
let mut path = Path::default(); let mut points = Vec::new();
if interaction.right && !interaction.bottom && !interaction.top { if interaction.right && !interaction.bottom && !interaction.top {
path.add_line_segment([pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]); points.push(pos2(max.x, min.y + cr));
points.push(pos2(max.x, max.y - cr));
} }
if interaction.right && interaction.bottom { if interaction.right && interaction.bottom {
path.add_line_segment([pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]); points.push(pos2(max.x, min.y + cr));
path.add_circle_quadrant(pos2(max.x - cr, max.y - cr), cr, 0.0); points.push(pos2(max.x, max.y - cr));
add_circle_quadrant(&mut points, pos2(max.x - cr, max.y - cr), cr, 0.0);
} }
if interaction.bottom { if interaction.bottom {
path.add_line_segment([pos2(max.x - cr, max.y), pos2(min.x + cr, max.y)]); points.push(pos2(max.x - cr, max.y));
points.push(pos2(min.x + cr, max.y));
} }
if interaction.left && interaction.bottom { if interaction.left && interaction.bottom {
path.add_circle_quadrant(pos2(min.x + cr, max.y - cr), cr, 1.0); add_circle_quadrant(&mut points, pos2(min.x + cr, max.y - cr), cr, 1.0);
} }
if interaction.left { if interaction.left {
path.add_line_segment([pos2(min.x, max.y - cr), pos2(min.x, min.y + cr)]); points.push(pos2(min.x, max.y - cr));
points.push(pos2(min.x, min.y + cr));
} }
if interaction.left && interaction.top { if interaction.left && interaction.top {
path.add_circle_quadrant(pos2(min.x + cr, min.y + cr), cr, 2.0); add_circle_quadrant(&mut points, pos2(min.x + cr, min.y + cr), cr, 2.0);
} }
if interaction.top { if interaction.top {
path.add_line_segment([pos2(min.x + cr, min.y), pos2(max.x - cr, min.y)]); points.push(pos2(min.x + cr, min.y));
points.push(pos2(max.x - cr, min.y));
} }
if interaction.right && interaction.top { if interaction.right && interaction.top {
path.add_circle_quadrant(pos2(max.x - cr, min.y + cr), cr, 3.0); add_circle_quadrant(&mut points, pos2(max.x - cr, min.y + cr), cr, 3.0);
path.add_line_segment([pos2(max.x, min.y + cr), pos2(max.x, max.y - cr)]); points.push(pos2(max.x, min.y + cr));
points.push(pos2(max.x, max.y - cr));
} }
ui.painter().add(PaintCmd::Path { ui.painter().add(PaintCmd::Path {
path, points,
closed: false, closed: false,
fill: Default::default(), fill: Default::default(),
outline: visuals.bg_outline, outline: visuals.bg_outline,

View file

@ -653,7 +653,7 @@ impl Painting {
if line.len() >= 2 { if line.len() >= 2 {
let points: Vec<Pos2> = line.iter().map(|p| rect.min + *p).collect(); let points: Vec<Pos2> = line.iter().map(|p| rect.min + *p).collect();
painter.add(PaintCmd::Path { painter.add(PaintCmd::Path {
path: Path::from_open_points(&points), points,
closed: false, closed: false,
outline: LineStyle::new(self.line_width, LIGHT_GRAY), outline: LineStyle::new(self.line_width, LIGHT_GRAY),
fill: Default::default(), fill: Default::default(),

View file

@ -13,6 +13,6 @@ pub use {
color::{Rgba, Srgba}, color::{Rgba, Srgba},
command::{LineStyle, PaintCmd}, command::{LineStyle, PaintCmd},
fonts::{FontDefinitions, Fonts, TextStyle}, fonts::{FontDefinitions, Fonts, TextStyle},
tessellator::{PaintJobs, PaintOptions, Path, Triangles, Vertex}, tessellator::{PaintJobs, PaintOptions, Triangles, Vertex},
texture_atlas::Texture, texture_atlas::Texture,
}; };

View file

@ -1,5 +1,5 @@
use { use {
super::{font::Galley, fonts::TextStyle, Path, Srgba, Triangles}, super::{font::Galley, fonts::TextStyle, Srgba, Triangles},
crate::math::{Pos2, Rect}, crate::math::{Pos2, Rect},
}; };
@ -19,7 +19,9 @@ pub enum PaintCmd {
style: LineStyle, style: LineStyle,
}, },
Path { Path {
path: Path, points: Vec<Pos2>,
/// If true, connect the first and last of the points together.
/// This is required if `fill != TRANSPARENT`.
closed: bool, closed: bool,
fill: Srgba, fill: Srgba,
outline: LineStyle, outline: LineStyle,

View file

@ -184,34 +184,15 @@ pub struct PathPoint {
/// A connected line (without thickness or gaps) which can be tessellated /// A connected line (without thickness or gaps) which can be tessellated
/// to either to an outline (with thickness) or a filled convex area. /// to either to an outline (with thickness) or a filled convex area.
/// Used as a scratch-pad during tesselation.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct Path(Vec<PathPoint>); struct Path(Vec<PathPoint>);
impl Path { impl Path {
pub fn from_point_loop(points: &[Pos2]) -> Self {
let mut path = Self::default();
path.add_line_loop(points);
path
}
pub fn from_open_points(points: &[Pos2]) -> Self {
let mut path = Self::default();
path.add_open_points(points);
path
}
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.0.clear(); self.0.clear();
} }
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn reserve(&mut self, additional: usize) { pub fn reserve(&mut self, additional: usize) {
self.0.reserve(additional) self.0.reserve(additional)
} }
@ -247,14 +228,15 @@ impl Path {
// Common case optimization: // Common case optimization:
self.add_line_segment([points[0], points[1]]); self.add_line_segment([points[0], points[1]]);
} else { } else {
// TODO: optimize
self.reserve(n); self.reserve(n);
self.add_point(points[0], (points[1] - points[0]).normalized().rot90()); self.add_point(points[0], (points[1] - points[0]).normalized().rot90());
for i in 1..n - 1 { for i in 1..n - 1 {
let n0 = (points[i] - points[i - 1]).normalized().rot90(); // TODO: don't calculate each normal twice! let n0 = (points[i] - points[i - 1]).normalized().rot90();
let n1 = (points[i + 1] - points[i]).normalized().rot90(); // TODO: don't calculate each normal twice! let n1 = (points[i + 1] - points[i]).normalized().rot90();
let v = (n0 + n1) / 2.0; let v = (n0 + n1) / 2.0;
let normal = v / v.length_sq(); let normal = v / v.length_sq(); // TODO: handle VERY sharp turns better
self.add_point(points[i], normal); // TODO: handle VERY sharp turns better self.add_point(points[i], normal);
} }
self.add_point( self.add_point(
points[n - 1], points[n - 1],
@ -273,22 +255,20 @@ impl Path {
let n0 = (points[i] - points[(i + n - 1) % n]).normalized().rot90(); let n0 = (points[i] - points[(i + n - 1) % n]).normalized().rot90();
let n1 = (points[(i + 1) % n] - points[i]).normalized().rot90(); let n1 = (points[(i + 1) % n] - points[i]).normalized().rot90();
let v = (n0 + n1) / 2.0; let v = (n0 + n1) / 2.0;
let normal = v / v.length_sq(); let normal = v / v.length_sq(); // TODO: handle VERY sharp turns better
self.add_point(points[i], normal); // TODO: handle VERY sharp turns better self.add_point(points[i], normal);
}
} }
} }
pub fn add_rectangle(&mut self, rect: Rect) { pub(crate) mod path {
let min = rect.min; //! Helpers for constructing paths
let max = rect.max; use super::*;
self.reserve(4);
self.add_point(pos2(min.x, min.y), vec2(-1.0, -1.0)); /// overwrites existing points
self.add_point(pos2(max.x, min.y), vec2(1.0, -1.0)); pub fn rounded_rectangle(path: &mut Vec<Pos2>, rect: Rect, corner_radius: f32) {
self.add_point(pos2(max.x, max.y), vec2(1.0, 1.0)); path.clear();
self.add_point(pos2(min.x, max.y), vec2(-1.0, 1.0));
}
pub fn add_rounded_rectangle(&mut self, rect: Rect, corner_radius: f32) {
let min = rect.min; let min = rect.min;
let max = rect.max; let max = rect.max;
@ -297,12 +277,18 @@ impl Path {
.min(rect.height() * 0.5); .min(rect.height() * 0.5);
if cr <= 0.0 { if cr <= 0.0 {
self.add_rectangle(rect); let min = rect.min;
let max = rect.max;
path.reserve(4);
path.push(pos2(min.x, min.y));
path.push(pos2(max.x, min.y));
path.push(pos2(max.x, max.y));
path.push(pos2(min.x, max.y));
} else { } else {
self.add_circle_quadrant(pos2(max.x - cr, max.y - cr), cr, 0.0); add_circle_quadrant(path, pos2(max.x - cr, max.y - cr), cr, 0.0);
self.add_circle_quadrant(pos2(min.x + cr, max.y - cr), cr, 1.0); add_circle_quadrant(path, pos2(min.x + cr, max.y - cr), cr, 1.0);
self.add_circle_quadrant(pos2(min.x + cr, min.y + cr), cr, 2.0); add_circle_quadrant(path, pos2(min.x + cr, min.y + cr), cr, 2.0);
self.add_circle_quadrant(pos2(max.x - cr, min.y + cr), cr, 3.0); add_circle_quadrant(path, pos2(max.x - cr, min.y + cr), cr, 3.0);
} }
} }
@ -324,21 +310,20 @@ impl Path {
// * angle 3 * TAU / 4 = top // * angle 3 * TAU / 4 = top
// - quadrant 3: right top // - quadrant 3: right top
// * angle 4 * TAU / 4 = right // * angle 4 * TAU / 4 = right
pub fn add_circle_quadrant(&mut self, center: Pos2, radius: f32, quadrant: f32) { pub fn add_circle_quadrant(path: &mut Vec<Pos2>, center: Pos2, radius: f32, quadrant: f32) {
// TODO: optimize with precalculated vertices for some radii ranges // TODO: optimize with precalculated vertices for some radii ranges
let n = (radius * 0.75).round() as i32; // TODO: tweak a bit more let n = (radius * 0.75).round() as i32; // TODO: tweak a bit more
let n = clamp(n, 2..=32); let n = clamp(n, 2..=32);
self.reserve(n as usize + 1);
const RIGHT_ANGLE: f32 = TAU / 4.0; const RIGHT_ANGLE: f32 = TAU / 4.0;
path.reserve(n as usize + 1);
for i in 0..=n { for i in 0..=n {
let angle = remap( let angle = remap(
i as f32, i as f32,
0.0..=n as f32, 0.0..=n as f32,
quadrant * RIGHT_ANGLE..=(quadrant + 1.0) * RIGHT_ANGLE, quadrant * RIGHT_ANGLE..=(quadrant + 1.0) * RIGHT_ANGLE,
); );
let normal = vec2(angle.cos(), angle.sin()); path.push(center + radius * Vec2::angled(angle));
self.add_point(center + radius * normal, normal);
} }
} }
} }
@ -374,12 +359,7 @@ impl Default for PaintOptions {
} }
/// Tesselate the given convex area into a polygon. /// Tesselate the given convex area into a polygon.
pub fn fill_closed_path( fn fill_closed_path(path: &[PathPoint], color: Srgba, options: PaintOptions, out: &mut Triangles) {
path: &[PathPoint],
color: Srgba,
options: PaintOptions,
out: &mut Triangles,
) {
if color == color::TRANSPARENT { if color == color::TRANSPARENT {
return; return;
} }
@ -420,7 +400,7 @@ pub fn fill_closed_path(
} }
/// Tesselate the given path as an outline with thickness. /// Tesselate the given path as an outline with thickness.
pub fn paint_path_outline( fn paint_path_outline(
path: &[PathPoint], path: &[PathPoint],
path_type: PathType, path_type: PathType,
style: LineStyle, style: LineStyle,
@ -584,11 +564,12 @@ fn mul_color(color: Srgba, factor: f32) -> Srgba {
/// * `out`: where the triangles are put /// * `out`: where the triangles are put
/// * `scratchpad_path`: if you plan to run `tessellate_paint_command` /// * `scratchpad_path`: if you plan to run `tessellate_paint_command`
/// many times, pass it a reference to the same `Path` to avoid excessive allocations. /// many times, pass it a reference to the same `Path` to avoid excessive allocations.
pub fn tessellate_paint_command( fn tessellate_paint_command(
command: PaintCmd, command: PaintCmd,
options: PaintOptions, options: PaintOptions,
fonts: &Fonts, fonts: &Fonts,
out: &mut Triangles, out: &mut Triangles,
scratchpad_points: &mut Vec<Pos2>,
scratchpad_path: &mut Path, scratchpad_path: &mut Path,
) { ) {
let path = scratchpad_path; let path = scratchpad_path;
@ -616,12 +597,18 @@ pub fn tessellate_paint_command(
paint_path_outline(&path.0, Open, style, options, out); paint_path_outline(&path.0, Open, style, options, out);
} }
PaintCmd::Path { PaintCmd::Path {
path, points,
closed, closed,
fill, fill,
outline, outline,
} => { } => {
if path.len() >= 2 { if points.len() >= 2 {
if closed {
path.add_line_loop(&points);
} else {
path.add_open_points(&points);
}
if fill != TRANSPARENT { if fill != TRANSPARENT {
debug_assert!( debug_assert!(
closed, closed,
@ -645,7 +632,8 @@ pub fn tessellate_paint_command(
rect.min = rect.min.max(pos2(-1e7, -1e7)); rect.min = rect.min.max(pos2(-1e7, -1e7));
rect.max = rect.max.min(pos2(1e7, 1e7)); rect.max = rect.max.min(pos2(1e7, 1e7));
path.add_rounded_rectangle(rect, corner_radius); path::rounded_rectangle(scratchpad_points, rect, corner_radius);
path.add_line_loop(scratchpad_points);
fill_closed_path(&path.0, fill, options, out); fill_closed_path(&path.0, fill, options, out);
paint_path_outline(&path.0, Closed, outline, options, out); paint_path_outline(&path.0, Closed, outline, options, out);
} }
@ -710,6 +698,7 @@ pub fn tessellate_paint_commands(
options: PaintOptions, options: PaintOptions,
fonts: &Fonts, fonts: &Fonts,
) -> Vec<(Rect, Triangles)> { ) -> Vec<(Rect, Triangles)> {
let mut scratchpad_points = Vec::new();
let mut scratchpad_path = Path::default(); let mut scratchpad_path = Path::default();
let mut jobs = PaintJobs::default(); let mut jobs = PaintJobs::default();
@ -721,7 +710,14 @@ pub fn tessellate_paint_commands(
} }
let out = &mut jobs.last_mut().unwrap().1; let out = &mut jobs.last_mut().unwrap().1;
tessellate_paint_command(cmd, options, fonts, out, &mut scratchpad_path); tessellate_paint_command(
cmd,
options,
fonts,
out,
&mut scratchpad_points,
&mut scratchpad_path,
);
} }
if options.debug_paint_clip_rects { if options.debug_paint_clip_rects {
@ -736,6 +732,7 @@ pub fn tessellate_paint_commands(
options, options,
fonts, fonts,
triangles, triangles,
&mut scratchpad_points,
&mut scratchpad_path, &mut scratchpad_path,
) )
} }

View file

@ -366,11 +366,11 @@ impl<'a> Widget for Checkbox<'a> {
if *checked { if *checked {
ui.painter().add(PaintCmd::Path { ui.painter().add(PaintCmd::Path {
path: Path::from_open_points(&[ points: vec![
pos2(small_icon_rect.left(), small_icon_rect.center().y), pos2(small_icon_rect.left(), small_icon_rect.center().y),
pos2(small_icon_rect.center().x, small_icon_rect.bottom()), pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
pos2(small_icon_rect.right(), small_icon_rect.top()), pos2(small_icon_rect.right(), small_icon_rect.top()),
]), ],
closed: false, closed: false,
outline: LineStyle::new(ui.style().visuals.line_width, stroke_color), outline: LineStyle::new(ui.style().visuals.line_width, stroke_color),
fill: Default::default(), fill: Default::default(),