From c321a363171358c7861d777b5e627caa535d4091 Mon Sep 17 00:00:00 2001 From: Federico Rinaldi Date: Wed, 18 Dec 2024 17:58:54 +0100 Subject: [PATCH] Reorganize the `shapes` module (#270) * Reorganize `shapes` module * Improve `shape` documentation --- rustfmt.toml | 2 +- src/shapes.rs | 552 ---------------------------------------- src/shapes/circle.rs | 32 +++ src/shapes/ellipse.rs | 41 +++ src/shapes/line.rs | 19 ++ src/shapes/mod.rs | 15 ++ src/shapes/polygon.rs | 182 +++++++++++++ src/shapes/rectangle.rs | 154 +++++++++++ src/shapes/svg.rs | 201 +++++++++++++++ 9 files changed, 645 insertions(+), 553 deletions(-) delete mode 100644 src/shapes.rs create mode 100644 src/shapes/circle.rs create mode 100644 src/shapes/ellipse.rs create mode 100644 src/shapes/line.rs create mode 100644 src/shapes/mod.rs create mode 100644 src/shapes/polygon.rs create mode 100644 src/shapes/rectangle.rs create mode 100644 src/shapes/svg.rs diff --git a/rustfmt.toml b/rustfmt.toml index a192179..94db92c 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -5,4 +5,4 @@ newline_style = "Unix" reorder_impl_items = true unstable_features = true use_field_init_shorthand = true -wrap_comments = true +wrap_comments = false diff --git a/src/shapes.rs b/src/shapes.rs deleted file mode 100644 index 693f3e4..0000000 --- a/src/shapes.rs +++ /dev/null @@ -1,552 +0,0 @@ -//! Collection of common shapes that can be drawn. -//! -//! The structs defined in this module implement the -//! [`Geometry`](crate::geometry::Geometry) trait. You can also implement -//! the trait for your own shapes. - -use bevy::math::Vec2; -use lyon_tessellation::{ - geom::euclid::default::Size2D, - math::{point, Angle, Box2D, Point, Vector}, - path::{ - builder::{BorderRadii as LyonBorderRadii, WithSvg}, - path::Builder, - traits::SvgPathBuilder, - ArcFlags, Polygon as LyonPolygon, Winding, - }, -}; -use svgtypes::{PathParser, PathSegment}; - -use crate::{ - geometry::Geometry, - utils::{ToPoint, ToVector}, -}; - -/// Defines where the origin, or pivot of the `Rectangle` should be positioned. -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum RectangleOrigin { - Center, - BottomLeft, - BottomRight, - TopRight, - TopLeft, - CustomCenter(Vec2), -} - -impl Default for RectangleOrigin { - fn default() -> Self { - Self::Center - } -} - -/// Radii for the four corners of a rectangle. -#[derive(Debug, Default, Clone, Copy, PartialEq)] -pub struct BorderRadii { - /// Radius for the top left corner. - pub top_left: f32, - /// Radius for the top right corner. - pub top_right: f32, - /// Radius for the bottom left corner. - pub bottom_left: f32, - /// Radius for the bottom right corner. - pub bottom_right: f32, -} - -impl BorderRadii { - /// Use a single radius for all corners. - #[must_use] - pub fn single(radius: f32) -> Self { - Self { - top_left: radius, - top_right: radius, - bottom_left: radius, - bottom_right: radius, - } - } - - /// Use a single radius for the top corners and zero for the bottom corners. - #[must_use] - pub fn top(radius: f32) -> Self { - Self { - top_left: radius, - top_right: radius, - ..Default::default() - } - } - - /// Use a single radius for the bottom corners and zero for the top corners. - #[must_use] - pub fn bottom(radius: f32) -> Self { - Self { - bottom_left: radius, - bottom_right: radius, - ..Default::default() - } - } - - /// Use a single radius for the left corners and zero for the right corners. - #[must_use] - pub fn left(radius: f32) -> Self { - Self { - top_left: radius, - bottom_left: radius, - ..Default::default() - } - } - - /// Use a single radius for the right corners and zero for the left corners. - #[must_use] - pub fn right(radius: f32) -> Self { - Self { - top_right: radius, - bottom_right: radius, - ..Default::default() - } - } -} - -impl From for LyonBorderRadii { - fn from(source: BorderRadii) -> Self { - // swap top and bottom - Self { - top_left: source.bottom_left.abs(), - top_right: source.bottom_right.abs(), - bottom_left: source.top_left.abs(), - bottom_right: source.top_right.abs(), - } - } -} - -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Rectangle { - pub extents: Vec2, - pub origin: RectangleOrigin, - pub radii: Option, -} - -impl Default for Rectangle { - fn default() -> Self { - Self { - extents: Vec2::ONE, - origin: RectangleOrigin::default(), - radii: None, - } - } -} - -impl Geometry for Rectangle { - fn add_geometry(&self, b: &mut Builder) { - let origin = match self.origin { - RectangleOrigin::Center => Point::new(-self.extents.x / 2.0, -self.extents.y / 2.0), - RectangleOrigin::BottomLeft => Point::new(0.0, 0.0), - RectangleOrigin::BottomRight => Point::new(-self.extents.x, 0.0), - RectangleOrigin::TopRight => Point::new(-self.extents.x, -self.extents.y), - RectangleOrigin::TopLeft => Point::new(0.0, -self.extents.y), - RectangleOrigin::CustomCenter(v) => { - Point::new(v.x - self.extents.x / 2.0, v.y - self.extents.y / 2.0) - } - }; - let rect = - &Box2D::from_origin_and_size(origin, Size2D::new(self.extents.x, self.extents.y)); - let Some(radii) = self.radii else { - b.add_rectangle(rect, Winding::Positive); - return; - }; - b.add_rounded_rectangle(rect, &radii.into(), Winding::Positive); - } -} - -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Circle { - pub radius: f32, - pub center: Vec2, -} - -impl Default for Circle { - fn default() -> Self { - Self { - radius: 1.0, - center: Vec2::ZERO, - } - } -} - -impl Geometry for Circle { - fn add_geometry(&self, b: &mut Builder) { - b.add_circle(self.center.to_point(), self.radius, Winding::Positive); - } -} - -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Ellipse { - pub radii: Vec2, - pub center: Vec2, -} - -impl Default for Ellipse { - fn default() -> Self { - Self { - radii: Vec2::ONE, - center: Vec2::ZERO, - } - } -} - -impl Geometry for Ellipse { - fn add_geometry(&self, b: &mut Builder) { - b.add_ellipse( - self.center.to_point(), - self.radii.to_vector(), - Angle::zero(), - Winding::Positive, - ); - } -} - -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq)] -pub struct Polygon { - pub points: Vec, - pub closed: bool, -} - -impl Default for Polygon { - fn default() -> Self { - Self { - points: Vec::new(), - closed: true, - } - } -} - -impl Geometry for Polygon { - fn add_geometry(&self, b: &mut Builder) { - let points = self - .points - .iter() - .map(|p| p.to_point()) - .collect::>(); - let polygon: LyonPolygon = LyonPolygon { - points: points.as_slice(), - closed: self.closed, - }; - - b.add_polygon(polygon); - } -} - -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq)] -pub struct RoundedPolygon { - pub points: Vec, - pub radius: f32, - pub closed: bool, -} - -impl Default for RoundedPolygon { - fn default() -> Self { - Self { - points: Vec::new(), - radius: 0.0, - closed: true, - } - } -} - -impl Geometry for RoundedPolygon { - fn add_geometry(&self, b: &mut Builder) { - let points = self - .points - .iter() - .map(|p| p.to_point()) - .collect::>(); - let polygon: LyonPolygon = LyonPolygon { - points: points.as_slice(), - closed: self.closed, - }; - lyon_algorithms::rounded_polygon::add_rounded_polygon( - b, - polygon, - self.radius, - lyon_algorithms::path::NO_ATTRIBUTES, - ); - } -} - -/// The regular polygon feature used to determine the dimensions of the polygon. -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum RegularPolygonFeature { - /// The radius of the polygon's circumcircle. - Radius(f32), - /// The radius of the polygon's incircle. - Apothem(f32), - /// The length of the polygon's side. - SideLength(f32), -} - -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct RegularPolygon { - pub sides: usize, - pub center: Vec2, - pub feature: RegularPolygonFeature, -} - -impl RegularPolygon { - /// Gets the radius of the polygon. - fn radius(&self) -> f32 { - let ratio = std::f32::consts::PI / self.sides as f32; - - match self.feature { - RegularPolygonFeature::Radius(r) => r, - RegularPolygonFeature::Apothem(a) => a * ratio.tan() / ratio.sin(), - RegularPolygonFeature::SideLength(s) => s / (2.0 * ratio.sin()), - } - } -} - -impl Default for RegularPolygon { - fn default() -> Self { - Self { - sides: 3, - center: Vec2::ZERO, - feature: RegularPolygonFeature::Radius(1.0), - } - } -} - -impl Geometry for RegularPolygon { - fn add_geometry(&self, b: &mut Builder) { - // -- Implementation details **PLEASE KEEP UPDATED** -- - // - `step`: angle between two vertices. - // - `internal`: internal angle of the polygon. - // - `offset`: bias to make the shape lay flat on a line parallel to the x-axis. - - use std::f32::consts::PI; - assert!(self.sides > 2, "Polygons must have at least 3 sides"); - let n = self.sides as f32; - let radius = self.radius(); - let internal = (n - 2.0) * PI / n; - let offset = -internal / 2.0; - - let mut points = Vec::with_capacity(self.sides); - let step = 2.0 * PI / n; - for i in 0..self.sides { - let cur_angle = (i as f32).mul_add(step, offset); - let x = radius.mul_add(cur_angle.cos(), self.center.x); - let y = radius.mul_add(cur_angle.sin(), self.center.y); - points.push(point(x, y)); - } - - let polygon = LyonPolygon { - points: points.as_slice(), - closed: true, - }; - - b.add_polygon(polygon); - } -} - -/// A simple line segment, specified by two points. -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Line(pub Vec2, pub Vec2); - -impl Geometry for Line { - fn add_geometry(&self, b: &mut Builder) { - b.add_polygon(LyonPolygon { - points: &[self.0.to_point(), self.1.to_point()], - closed: false, - }); - } -} -///An easy way to display svg paths as a shape, takes an svg path string and a -///document size(Vec2). -/// -///For documentation on svg paths: -/// -///Make sure that your units are pixels(px) and that the transform of the \ -///in your svg document is set to transform="translate(0,0)" so as to not -///offset the coordinates of the paths -/// -///In inkscape for example, to turn your units into pixels, you: -/// 1) Go to File>Document Properties>General>Display Units and set it to px -/// -/// 2) In File>Document Properties>Custom Size>Units set it to px, also, this -/// size would be used for `svg_doc_size_in_px` -/// -/// 3) In File>Document Properties>Scale>Scale x make sure it is set to 1 User -/// unit per px -/// -///Example exists in the examples folder -pub struct SvgPathShape { - ///The document size of the svg art, make sure the units are in pixels - pub svg_doc_size_in_px: Vec2, - ///The string that describes the path, make sure the units are in pixels - ///and that the transform of the \ in your svg document is set to - ///transform="translate(0,0)" so as to not offset the coordinates of the - ///paths - pub svg_path_string: String, -} -fn get_y_in_bevy_orientation(y: f64) -> f32 { - y as f32 * -1. -} -fn get_y_after_offset(y: f64, offset_y: f32) -> f32 { - get_y_in_bevy_orientation(y) + offset_y -} -fn get_x_after_offset(x: f64, offset_x: f32) -> f32 { - x as f32 - offset_x -} -fn get_point_after_offset(x: f64, y: f64, offset_x: f32, offset_y: f32) -> Point { - Point::new( - get_x_after_offset(x, offset_x), - get_y_after_offset(y, offset_y), - ) -} -fn get_corrected_relative_vector(x: f64, y: f64) -> Vector { - Vector::new(x as f32, get_y_in_bevy_orientation(y)) -} -impl Geometry for SvgPathShape { - #[allow(clippy::too_many_lines)] - fn add_geometry(&self, b: &mut Builder) { - let builder = Builder::new(); - let mut svg_builder = WithSvg::new(builder); - let offset_x = self.svg_doc_size_in_px.x / 2.; - let offset_y = self.svg_doc_size_in_px.y / 2.; - let mut used_move_command = false; - - for path_segment in PathParser::from(self.svg_path_string.as_str()) { - match path_segment.unwrap() { - PathSegment::MoveTo { abs, x, y } => { - if abs || !used_move_command { - svg_builder.move_to(get_point_after_offset(x, y, offset_x, offset_y)); - used_move_command = true; - } else { - svg_builder.relative_move_to(get_corrected_relative_vector(x, y)); - } - } - PathSegment::LineTo { abs, x, y } => { - if abs { - svg_builder.line_to(get_point_after_offset(x, y, offset_x, offset_y)); - } else { - svg_builder.relative_line_to(get_corrected_relative_vector(x, y)); - } - } - PathSegment::HorizontalLineTo { abs, x } => { - if abs { - svg_builder.horizontal_line_to(get_x_after_offset(x, offset_x)); - } else { - svg_builder.relative_horizontal_line_to(x as f32); - } - } - PathSegment::VerticalLineTo { abs, y } => { - if abs { - svg_builder.vertical_line_to(get_y_after_offset(y, offset_y)); - } else { - svg_builder.relative_vertical_line_to(get_y_in_bevy_orientation(y)); - } - } - PathSegment::CurveTo { - abs, - x1, - y1, - x2, - y2, - x, - y, - } => { - if abs { - svg_builder.cubic_bezier_to( - get_point_after_offset(x1, y1, offset_x, offset_y), - get_point_after_offset(x2, y2, offset_x, offset_y), - get_point_after_offset(x, y, offset_x, offset_y), - ); - } else { - svg_builder.relative_cubic_bezier_to( - get_corrected_relative_vector(x1, y1), - get_corrected_relative_vector(x2, y2), - get_corrected_relative_vector(x, y), - ); - } - } - PathSegment::SmoothCurveTo { abs, x2, y2, x, y } => { - if abs { - svg_builder.smooth_cubic_bezier_to( - get_point_after_offset(x2, y2, offset_x, offset_y), - get_point_after_offset(x, y, offset_x, offset_y), - ); - } else { - svg_builder.smooth_relative_cubic_bezier_to( - get_corrected_relative_vector(x2, y2), - get_corrected_relative_vector(x, y), - ); - } - } - PathSegment::Quadratic { abs, x1, y1, x, y } => { - if abs { - svg_builder.quadratic_bezier_to( - get_point_after_offset(x1, y1, offset_x, offset_y), - get_point_after_offset(x, y, offset_x, offset_y), - ); - } else { - svg_builder.relative_quadratic_bezier_to( - get_corrected_relative_vector(x1, y1), - get_corrected_relative_vector(x, y), - ); - } - } - PathSegment::SmoothQuadratic { abs, x, y } => { - if abs { - svg_builder.smooth_quadratic_bezier_to(get_point_after_offset( - x, y, offset_x, offset_y, - )); - } else { - svg_builder.smooth_relative_quadratic_bezier_to( - get_corrected_relative_vector(x, y), - ); - } - } - PathSegment::EllipticalArc { - abs, - rx, - ry, - x_axis_rotation, - large_arc, - sweep, - x, - y, - } => { - if abs { - svg_builder.arc_to( - Vector::new(rx as f32, ry as f32), - Angle { - radians: x_axis_rotation as f32, - }, - ArcFlags { large_arc, sweep }, - get_point_after_offset(x, y, offset_x, offset_y), - ); - } else { - svg_builder.relative_arc_to( - Vector::new(rx as f32, ry as f32), - Angle { - radians: x_axis_rotation as f32, - }, - ArcFlags { large_arc, sweep }, - get_corrected_relative_vector(x, y), - ); - } - } - PathSegment::ClosePath { abs: _ } => { - svg_builder.close(); - } - } - } - let path = svg_builder.build(); - b.extend_from_paths(&[path.as_slice()]); - } -} diff --git a/src/shapes/circle.rs b/src/shapes/circle.rs new file mode 100644 index 0000000..c579b4f --- /dev/null +++ b/src/shapes/circle.rs @@ -0,0 +1,32 @@ +//! Tools for drawing circles. + +use bevy::math::Vec2; +use lyon_tessellation::path::{path::Builder, Winding}; + +use crate::{geometry::Geometry, utils::ToPoint}; + +/// A shape where all points are equidistant from a fixed point. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Circle { + /// The distance from the [`center`] of any point of the circumference. + /// + /// [`center`]: Self::center + pub radius: f32, + /// Point equidistant from all points of the circumference. + pub center: Vec2, +} + +impl Default for Circle { + fn default() -> Self { + Self { + radius: 1.0, + center: Vec2::ZERO, + } + } +} + +impl Geometry for Circle { + fn add_geometry(&self, b: &mut Builder) { + b.add_circle(self.center.to_point(), self.radius, Winding::Positive); + } +} diff --git a/src/shapes/ellipse.rs b/src/shapes/ellipse.rs new file mode 100644 index 0000000..53ea41a --- /dev/null +++ b/src/shapes/ellipse.rs @@ -0,0 +1,41 @@ +//! Tools for drawing ellipses. + +use bevy::math::Vec2; +use lyon_tessellation::{ + math::Angle, + path::{path::Builder, Winding}, +}; + +use crate::{ + geometry::Geometry, + utils::{ToPoint, ToVector}, +}; + +/// A shape similar to a circle under a non-uniform scaling. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Ellipse { + /// The horizontal and vertical radii. + pub radii: Vec2, + /// The midpoint of the horizontal and vertical axes. + pub center: Vec2, +} + +impl Default for Ellipse { + fn default() -> Self { + Self { + radii: Vec2::ONE, + center: Vec2::ZERO, + } + } +} + +impl Geometry for Ellipse { + fn add_geometry(&self, b: &mut Builder) { + b.add_ellipse( + self.center.to_point(), + self.radii.to_vector(), + Angle::zero(), + Winding::Positive, + ); + } +} diff --git a/src/shapes/line.rs b/src/shapes/line.rs new file mode 100644 index 0000000..5545341 --- /dev/null +++ b/src/shapes/line.rs @@ -0,0 +1,19 @@ +//! Tools for drawing line segments. + +use bevy::math::Vec2; +use lyon_tessellation::path::{path::Builder, Polygon as LyonPolygon}; + +use crate::{geometry::Geometry, utils::ToPoint}; + +/// A line segment, specified by two points. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Line(pub Vec2, pub Vec2); + +impl Geometry for Line { + fn add_geometry(&self, b: &mut Builder) { + b.add_polygon(LyonPolygon { + points: &[self.0.to_point(), self.1.to_point()], + closed: false, + }); + } +} diff --git a/src/shapes/mod.rs b/src/shapes/mod.rs new file mode 100644 index 0000000..2e98121 --- /dev/null +++ b/src/shapes/mod.rs @@ -0,0 +1,15 @@ +//! Tools for drawing common shapes. + +pub mod circle; +pub mod ellipse; +pub mod line; +pub mod polygon; +pub mod rectangle; +pub mod svg; + +pub use circle::*; +pub use ellipse::*; +pub use line::*; +pub use polygon::*; +pub use rectangle::*; +pub use svg::*; diff --git a/src/shapes/polygon.rs b/src/shapes/polygon.rs new file mode 100644 index 0000000..7bb1216 --- /dev/null +++ b/src/shapes/polygon.rs @@ -0,0 +1,182 @@ +//! Tools for drawing polygons. + +use bevy::math::Vec2; +use lyon_tessellation::{ + math::{point, Point}, + path::{path::Builder, Polygon as LyonPolygon}, +}; + +use crate::{geometry::Geometry, utils::ToPoint}; + +/// An arbitrary polygon or polyline. +#[derive(Debug, Clone, PartialEq)] +pub struct Polygon { + /// The vertices of a polygon. + /// + /// Each vertex is connected to the next. + /// The last vertex is connected to the first, + /// if [`closed`] is `true`. + /// + /// [`closed`]: Self::closed + pub points: Vec, + /// Whether the last vertex is connected to the first. + /// + /// If `true`, the shape is a *polygon*. + /// If `false`, the shape is a *polygonal line* (also called *polyline*). + pub closed: bool, +} + +impl Default for Polygon { + fn default() -> Self { + Self { + points: Vec::new(), + closed: true, + } + } +} + +impl Geometry for Polygon { + fn add_geometry(&self, b: &mut Builder) { + let points = self + .points + .iter() + .map(|p| p.to_point()) + .collect::>(); + let polygon: LyonPolygon = LyonPolygon { + points: points.as_slice(), + closed: self.closed, + }; + + b.add_polygon(polygon); + } +} + +/// An arbitrary polygon, or polyline, with rounded vertices. +#[derive(Debug, Clone, PartialEq)] +pub struct RoundedPolygon { + /// The vertices of a polygon. + /// + /// Each vertex is connected to the next. + /// The last vertex is connected to the first, + /// if `closed` is true. + pub points: Vec, + /// The radius of the arc used to round the corners of the vertices. + pub radius: f32, + /// Whether the last vertex is connected to the first. + /// + /// If `true`, the shape is a *polygon*. + /// If `false`, the shape is a *polygonal line* (also called *polyline*). + pub closed: bool, +} + +impl Default for RoundedPolygon { + fn default() -> Self { + Self { + points: Vec::new(), + radius: 0.0, + closed: true, + } + } +} + +impl Geometry for RoundedPolygon { + fn add_geometry(&self, b: &mut Builder) { + let points = self + .points + .iter() + .map(|p| p.to_point()) + .collect::>(); + let polygon: LyonPolygon = LyonPolygon { + points: points.as_slice(), + closed: self.closed, + }; + lyon_algorithms::rounded_polygon::add_rounded_polygon( + b, + polygon, + self.radius, + lyon_algorithms::path::NO_ATTRIBUTES, + ); + } +} + +/// The geometric property used to define a [`RegularPolygon`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum RegularPolygonFeature { + /// The radius of the polygon's circumcircle. + Radius(f32), + /// The radius of the polygon's incircle. + Apothem(f32), + /// The length of the polygon's side. + SideLength(f32), +} + +/// A polygon with all sides and angles equal. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct RegularPolygon { + /// The number of sides. + /// + /// # Panics + /// + /// Attempting to draw a `RegularPolygon` with a value less than `3` + /// will result in a panic. + pub sides: usize, + /// The point equidistant from all its vertices. + pub center: Vec2, + /// The geometric property used to define the polygon. + pub feature: RegularPolygonFeature, +} + +impl RegularPolygon { + /// Gets the radius of the polygon. + fn radius(&self) -> f32 { + let ratio = std::f32::consts::PI / self.sides as f32; + + match self.feature { + RegularPolygonFeature::Radius(r) => r, + RegularPolygonFeature::Apothem(a) => a * ratio.tan() / ratio.sin(), + RegularPolygonFeature::SideLength(s) => s / (2.0 * ratio.sin()), + } + } +} + +impl Default for RegularPolygon { + fn default() -> Self { + Self { + sides: 3, + center: Vec2::ZERO, + feature: RegularPolygonFeature::Radius(1.0), + } + } +} + +impl Geometry for RegularPolygon { + fn add_geometry(&self, b: &mut Builder) { + // -- Implementation details **PLEASE KEEP UPDATED** -- + // - `step`: angle between two vertices. + // - `internal`: internal angle of the polygon. + // - `offset`: bias to make the shape lay flat on a line parallel to the x-axis. + + use std::f32::consts::PI; + assert!(self.sides > 2, "Polygons must have at least 3 sides"); + let n = self.sides as f32; + let radius = self.radius(); + let internal = (n - 2.0) * PI / n; + let offset = -internal / 2.0; + + let mut points = Vec::with_capacity(self.sides); + let step = 2.0 * PI / n; + for i in 0..self.sides { + let cur_angle = (i as f32).mul_add(step, offset); + let x = radius.mul_add(cur_angle.cos(), self.center.x); + let y = radius.mul_add(cur_angle.sin(), self.center.y); + points.push(point(x, y)); + } + + let polygon = LyonPolygon { + points: points.as_slice(), + closed: true, + }; + + b.add_polygon(polygon); + } +} diff --git a/src/shapes/rectangle.rs b/src/shapes/rectangle.rs new file mode 100644 index 0000000..e552e95 --- /dev/null +++ b/src/shapes/rectangle.rs @@ -0,0 +1,154 @@ +//! Tools for drawing rectangles. + +use bevy::math::Vec2; +use lyon_tessellation::{ + geom::euclid::default::Size2D, + math::{Box2D, Point}, + path::{builder::BorderRadii as LyonBorderRadii, path::Builder, Winding}, +}; + +use crate::geometry::Geometry; + +/// Defines the frame of reference for the extents of a [`Rectangle`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum RectangleOrigin { + /// The extents of the rectangle are drawn relative to the center. + Center, + /// The extents of the rectangle are drawn from the bottom-left corner. + BottomLeft, + /// The extents of the rectangle are drawn from the bottom-right corner. + BottomRight, + /// The extents of the rectangle are drawn from the top-right corner. + TopRight, + /// The extents of the rectangle are drawn from the top-left corner. + TopLeft, + /// The extents of the rectangle are drawn relative to a custom point. + CustomCenter(Vec2), +} + +impl Default for RectangleOrigin { + fn default() -> Self { + Self::Center + } +} + +/// Radii for arcs rounding the corners of a [`Rectangle`]. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct BorderRadii { + /// Radius for the top left corner. + pub top_left: f32, + /// Radius for the top right corner. + pub top_right: f32, + /// Radius for the bottom left corner. + pub bottom_left: f32, + /// Radius for the bottom right corner. + pub bottom_right: f32, +} + +impl BorderRadii { + /// Use a single radius for all corners. + #[must_use] + pub fn single(radius: f32) -> Self { + Self { + top_left: radius, + top_right: radius, + bottom_left: radius, + bottom_right: radius, + } + } + + /// Use a single radius for the top corners and zero for the bottom corners. + #[must_use] + pub fn top(radius: f32) -> Self { + Self { + top_left: radius, + top_right: radius, + ..Default::default() + } + } + + /// Use a single radius for the bottom corners and zero for the top corners. + #[must_use] + pub fn bottom(radius: f32) -> Self { + Self { + bottom_left: radius, + bottom_right: radius, + ..Default::default() + } + } + + /// Use a single radius for the left corners and zero for the right corners. + #[must_use] + pub fn left(radius: f32) -> Self { + Self { + top_left: radius, + bottom_left: radius, + ..Default::default() + } + } + + /// Use a single radius for the right corners and zero for the left corners. + #[must_use] + pub fn right(radius: f32) -> Self { + Self { + top_right: radius, + bottom_right: radius, + ..Default::default() + } + } +} + +impl From for LyonBorderRadii { + fn from(source: BorderRadii) -> Self { + // swap top and bottom + Self { + top_left: source.bottom_left.abs(), + top_right: source.bottom_right.abs(), + bottom_left: source.top_left.abs(), + bottom_right: source.top_right.abs(), + } + } +} + +/// A quadrilateral with all internal right angles. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Rectangle { + /// The width and the height. + pub extents: Vec2, + /// The frame of reference for the `extents`. + pub origin: RectangleOrigin, + /// Radii to round the corners of the rectangle with arcs. + pub radii: Option, +} + +impl Default for Rectangle { + fn default() -> Self { + Self { + extents: Vec2::ONE, + origin: RectangleOrigin::default(), + radii: None, + } + } +} + +impl Geometry for Rectangle { + fn add_geometry(&self, b: &mut Builder) { + let origin = match self.origin { + RectangleOrigin::Center => Point::new(-self.extents.x / 2.0, -self.extents.y / 2.0), + RectangleOrigin::BottomLeft => Point::new(0.0, 0.0), + RectangleOrigin::BottomRight => Point::new(-self.extents.x, 0.0), + RectangleOrigin::TopRight => Point::new(-self.extents.x, -self.extents.y), + RectangleOrigin::TopLeft => Point::new(0.0, -self.extents.y), + RectangleOrigin::CustomCenter(v) => { + Point::new(v.x - self.extents.x / 2.0, v.y - self.extents.y / 2.0) + } + }; + let rect = + &Box2D::from_origin_and_size(origin, Size2D::new(self.extents.x, self.extents.y)); + let Some(radii) = self.radii else { + b.add_rectangle(rect, Winding::Positive); + return; + }; + b.add_rounded_rectangle(rect, &radii.into(), Winding::Positive); + } +} diff --git a/src/shapes/svg.rs b/src/shapes/svg.rs new file mode 100644 index 0000000..01db587 --- /dev/null +++ b/src/shapes/svg.rs @@ -0,0 +1,201 @@ +//! Tools for drawing shapes from SVG strings. + +use bevy::math::Vec2; +use lyon_tessellation::{ + math::{Angle, Point, Vector}, + path::{builder::WithSvg, path::Builder, traits::SvgPathBuilder, ArcFlags}, +}; +use svgtypes::{PathParser, PathSegment}; + +use crate::geometry::Geometry; + +/// Defines a geometric shape from an SVG path. +/// +/// # Requirements +/// +/// All units must be in pixels. +/// See the documentation for each field for more details. +/// +/// In Inkscape, to turn units into pixels: +/// +/// 1) Go to +/// `File > Document Properties > General > Display Units`, +/// and set it to `px`. +/// +/// 2) Go to +/// `File > Document Properties > Custom Size > Units`, +/// and set it to `px`. +/// The same size should also be used for `svg_doc_size_in_px`. +/// +/// 3) Go to +/// `File > Document Properties > Scale > Scale x`, +/// and make sure it is set to `1 User unit per px`. +pub struct SvgPathShape { + /// The size of the SVG document. + /// + /// Units must be in pixels. + pub svg_doc_size_in_px: Vec2, + /// The string containing the SVG path. + /// + /// Any `` element must have its `transform` attribute set to + /// `transform="translate(0,0)"` + /// to avoid unwanted offsets in the coordinates of the path. + pub svg_path_string: String, +} +fn get_y_in_bevy_orientation(y: f64) -> f32 { + y as f32 * -1. +} +fn get_y_after_offset(y: f64, offset_y: f32) -> f32 { + get_y_in_bevy_orientation(y) + offset_y +} +fn get_x_after_offset(x: f64, offset_x: f32) -> f32 { + x as f32 - offset_x +} +fn get_point_after_offset(x: f64, y: f64, offset_x: f32, offset_y: f32) -> Point { + Point::new( + get_x_after_offset(x, offset_x), + get_y_after_offset(y, offset_y), + ) +} +fn get_corrected_relative_vector(x: f64, y: f64) -> Vector { + Vector::new(x as f32, get_y_in_bevy_orientation(y)) +} +impl Geometry for SvgPathShape { + #[allow(clippy::too_many_lines)] + fn add_geometry(&self, b: &mut Builder) { + let builder = Builder::new(); + let mut svg_builder = WithSvg::new(builder); + let offset_x = self.svg_doc_size_in_px.x / 2.; + let offset_y = self.svg_doc_size_in_px.y / 2.; + let mut used_move_command = false; + + for path_segment in PathParser::from(self.svg_path_string.as_str()) { + match path_segment.unwrap() { + PathSegment::MoveTo { abs, x, y } => { + if abs || !used_move_command { + svg_builder.move_to(get_point_after_offset(x, y, offset_x, offset_y)); + used_move_command = true; + } else { + svg_builder.relative_move_to(get_corrected_relative_vector(x, y)); + } + } + PathSegment::LineTo { abs, x, y } => { + if abs { + svg_builder.line_to(get_point_after_offset(x, y, offset_x, offset_y)); + } else { + svg_builder.relative_line_to(get_corrected_relative_vector(x, y)); + } + } + PathSegment::HorizontalLineTo { abs, x } => { + if abs { + svg_builder.horizontal_line_to(get_x_after_offset(x, offset_x)); + } else { + svg_builder.relative_horizontal_line_to(x as f32); + } + } + PathSegment::VerticalLineTo { abs, y } => { + if abs { + svg_builder.vertical_line_to(get_y_after_offset(y, offset_y)); + } else { + svg_builder.relative_vertical_line_to(get_y_in_bevy_orientation(y)); + } + } + PathSegment::CurveTo { + abs, + x1, + y1, + x2, + y2, + x, + y, + } => { + if abs { + svg_builder.cubic_bezier_to( + get_point_after_offset(x1, y1, offset_x, offset_y), + get_point_after_offset(x2, y2, offset_x, offset_y), + get_point_after_offset(x, y, offset_x, offset_y), + ); + } else { + svg_builder.relative_cubic_bezier_to( + get_corrected_relative_vector(x1, y1), + get_corrected_relative_vector(x2, y2), + get_corrected_relative_vector(x, y), + ); + } + } + PathSegment::SmoothCurveTo { abs, x2, y2, x, y } => { + if abs { + svg_builder.smooth_cubic_bezier_to( + get_point_after_offset(x2, y2, offset_x, offset_y), + get_point_after_offset(x, y, offset_x, offset_y), + ); + } else { + svg_builder.smooth_relative_cubic_bezier_to( + get_corrected_relative_vector(x2, y2), + get_corrected_relative_vector(x, y), + ); + } + } + PathSegment::Quadratic { abs, x1, y1, x, y } => { + if abs { + svg_builder.quadratic_bezier_to( + get_point_after_offset(x1, y1, offset_x, offset_y), + get_point_after_offset(x, y, offset_x, offset_y), + ); + } else { + svg_builder.relative_quadratic_bezier_to( + get_corrected_relative_vector(x1, y1), + get_corrected_relative_vector(x, y), + ); + } + } + PathSegment::SmoothQuadratic { abs, x, y } => { + if abs { + svg_builder.smooth_quadratic_bezier_to(get_point_after_offset( + x, y, offset_x, offset_y, + )); + } else { + svg_builder.smooth_relative_quadratic_bezier_to( + get_corrected_relative_vector(x, y), + ); + } + } + PathSegment::EllipticalArc { + abs, + rx, + ry, + x_axis_rotation, + large_arc, + sweep, + x, + y, + } => { + if abs { + svg_builder.arc_to( + Vector::new(rx as f32, ry as f32), + Angle { + radians: x_axis_rotation as f32, + }, + ArcFlags { large_arc, sweep }, + get_point_after_offset(x, y, offset_x, offset_y), + ); + } else { + svg_builder.relative_arc_to( + Vector::new(rx as f32, ry as f32), + Angle { + radians: x_axis_rotation as f32, + }, + ArcFlags { large_arc, sweep }, + get_corrected_relative_vector(x, y), + ); + } + } + PathSegment::ClosePath { abs: _ } => { + svg_builder.close(); + } + } + } + let path = svg_builder.build(); + b.extend_from_paths(&[path.as_slice()]); + } +}