From 17de89519c41905a3f9e7ec7373eabfcb08752e5 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Tue, 11 Jul 2023 12:17:40 -0700 Subject: [PATCH] Added stroked shape drawing --- src/render.rs | 2 +- src/shapes.rs | 135 +++++++++++++++++++++++++++++++++++++++++++++++++- src/text.rs | 6 +-- 3 files changed, 138 insertions(+), 5 deletions(-) diff --git a/src/render.rs b/src/render.rs index cc9e46d11..1f9f7dadc 100644 --- a/src/render.rs +++ b/src/render.rs @@ -288,7 +288,7 @@ mod text { /// used. /// /// `origin` allows controlling how the text will be drawn relative to the - /// coordinate provided in [`render()`](PreparedGraphic::render). + /// coordinate provided in [`render()`](crate::PreparedGraphic::render). pub fn draw_text_buffer( &mut self, buffer: &cosmic_text::Buffer, diff --git a/src/shapes.rs b/src/shapes.rs index 7bb4422cf..c4773249c 100644 --- a/src/shapes.rs +++ b/src/shapes.rs @@ -5,9 +5,10 @@ use figures::units::UPx; use figures::{Point, Rect}; use lyon_tessellation::{ FillGeometryBuilder, FillOptions, FillTessellator, FillVertex, FillVertexConstructor, - GeometryBuilder, GeometryBuilderError, StrokeGeometryBuilder, StrokeVertex, + GeometryBuilder, GeometryBuilderError, StrokeGeometryBuilder, StrokeTessellator, StrokeVertex, StrokeVertexConstructor, VertexId, }; +pub use lyon_tessellation::{LineCap, LineJoin}; use crate::pipeline::Vertex; use crate::{sealed, Color, Graphics, PreparedGraphic, ShapeSource, Texture, TextureSource}; @@ -47,6 +48,25 @@ impl Shape { path.fill(color) } + /// Returns a rectangle that has its outline stroked with `color` and + /// `options`. + pub fn stroked_rect( + rect: Rect, + color: Color, + options: StrokeOptions, + ) -> Shape + where + Unit: Add + Ord + FloatConversion + Copy, + { + let (p1, p2) = rect.extents(); + let path = PathBuilder::new(p1) + .line_to(Point::new(p2.x, p1.y)) + .line_to(p2) + .line_to(Point::new(p1.x, p2.y)) + .close(); + path.stroke(color, options) + } + /// Uploads the shape to the GPU. #[must_use] pub fn prepare(&self, graphics: &Graphics<'_>) -> PreparedGraphic @@ -349,6 +369,29 @@ where .expect("should not fail to tesselat4e a rect"); shape_builder.shape } + + /// Strokes this path with `color` and `options`. + /// + /// If this is a textured image, the sampled texture colors will be + /// multiplied with this color. To render the image unchanged, use + /// [`Color::WHITE`]. + #[must_use] + pub fn stroke(&self, color: Color, options: StrokeOptions) -> Shape { + let lyon_path = self.as_lyon(); + let mut shape_builder = ShapeBuilder::new(color); + let mut tesselator = StrokeTessellator::new(); + + tesselator + .tessellate_with_ids( + lyon_path.id_iter(), + &lyon_path, + Some(&lyon_path), + &options.into(), + &mut shape_builder, + ) + .expect("should not fail to tesselat4e a rect"); + shape_builder.shape + } } /// Builds a [`Path`]. @@ -565,3 +608,93 @@ where self.build() } } + +/// Options for stroking lines on a path. +pub struct StrokeOptions { + /// The width of the line. + pub line_width: Unit, + + /// See the SVG specification. + /// + /// Default value: `LineJoin::Miter`. + pub line_join: LineJoin, + + /// What cap to use at the start of each sub-path. + /// + /// Default value: `LineCap::Butt`. + pub start_cap: LineCap, + + /// What cap to use at the end of each sub-path. + /// + /// Default value: `LineCap::Butt`. + pub end_cap: LineCap, + + /// See the SVG specification. + /// + /// Must be greater than or equal to 1.0. + /// Default value: `StrokeOptions::DEFAULT_MITER_LIMIT`. + pub miter_limit: f32, + + /// Maximum allowed distance to the path when building an approximation. + /// + /// See [Flattening and tolerance](index.html#flattening-and-tolerance). + /// Default value: `StrokeOptions::DEFAULT_TOLERANCE`. + pub tolerance: f32, +} + +impl Default for StrokeOptions +where + Unit: DefaultStrokeWidth, +{ + fn default() -> Self { + Self { + line_width: Unit::default_stroke_width(), + line_join: lyon_tessellation::StrokeOptions::DEFAULT_LINE_JOIN, + start_cap: lyon_tessellation::StrokeOptions::DEFAULT_LINE_CAP, + end_cap: lyon_tessellation::StrokeOptions::DEFAULT_LINE_CAP, + miter_limit: lyon_tessellation::StrokeOptions::DEFAULT_MITER_LIMIT, + tolerance: Default::default(), + } + } +} + +/// Controls the default stroke width for a given unit. +pub trait DefaultStrokeWidth { + /// Returns the default width of a line stroked in this unit. + fn default_stroke_width() -> Self; +} + +impl DefaultStrokeWidth for figures::units::Lp { + /// Returns [`Self::points(1)`]. + fn default_stroke_width() -> Self { + Self::points(1) + } +} +impl DefaultStrokeWidth for figures::units::Px { + fn default_stroke_width() -> Self { + Self(1) + } +} + +impl From> for lyon_tessellation::StrokeOptions +where + Unit: FloatConversion, +{ + fn from(options: StrokeOptions) -> Self { + let StrokeOptions { + line_width, + line_join, + start_cap, + end_cap, + miter_limit, + tolerance, + } = options; + Self::default() + .with_line_width(line_width.into_float()) + .with_line_join(line_join) + .with_start_cap(start_cap) + .with_end_cap(end_cap) + .with_miter_limit(miter_limit) + .with_tolerance(tolerance) + } +} diff --git a/src/text.rs b/src/text.rs index 77d698d9f..84219a304 100644 --- a/src/text.rs +++ b/src/text.rs @@ -396,7 +396,7 @@ where Unit: ScreenScale + Sub + Copy + Debug, { // TODO the returned type should be able to be drawn, so that we don't have to call update_scratch_buffer again. - let line_height = dbg!(Unit::from_lp(kludgine.text.line_height, kludgine.scale)); + let line_height = Unit::from_lp(kludgine.text.line_height, kludgine.scale); let mut min = Point::new(Px::MAX, Px::MAX); let mut max = Point::new(Px::MIN, Px::MIN); map_each_glyph( @@ -413,8 +413,8 @@ where ); MeasuredText { - ascent: line_height - dbg!(Unit::from_px(min.y, kludgine.scale)), - descent: line_height - dbg!(Unit::from_px(max.y, kludgine.scale)), + ascent: line_height - Unit::from_px(min.y, kludgine.scale), + descent: line_height - Unit::from_px(max.y, kludgine.scale), left: Unit::from_px(min.x, kludgine.scale), width: Unit::from_px(max.x, kludgine.scale), }