From 0dacb384dcace5ce82f890b3b5cbdce2941ee461 Mon Sep 17 00:00:00 2001 From: Federico Rinaldi Date: Sat, 14 Dec 2024 17:48:53 +0100 Subject: [PATCH] Remove the need for `PathBuilder` (#267) * Make `Geometry` generic over the builder This is quite a rudimentary solution that involves too many traits. A minimal solution is needed to simplify the code. Fix test * Substitute `PathBuilder` with `ShapePath` `ShapePath` is a `Geometry` that can be fed to `ShapeBuilder`. This makes `PathBuilder` redundant. * Update `CHANGELOG.md` --- CHANGELOG.md | 7 ++- examples/path.rs | 15 +++-- src/entity.rs | 5 +- src/geometry.rs | 83 +++++++++++++++++-------- src/lib.rs | 4 +- src/path.rs | 157 ++++++++++++++++++++++++++--------------------- src/shapes.rs | 16 ++--- 7 files changed, 173 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b8787c..2085e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.14.0 This version uses Bevy's **Required Components**, +along with other changes, therefore many items have been changed. Examples may help migration. @@ -12,8 +13,10 @@ Examples may help migration. - Deprecated `ShapeBundle` in favor of the `Shape` component. - `prelude` no longer exports `ShapeBundle`. - Added `ShapeBuilder`: works similarly to `GeometryBuilder` -- Removed `GeometryBuilder` and `ShapePath`. -- `PathBuilder` now allows chaining methods. +- Removed `GeometryBuilder` and `PathBuilder`. +- `ShapePath` now works similarly to `PathBuilder`, + but implements `Geometry`, + so it has to be used with `ShapeBuilder`. ## 0.13.0 - Support for Bevy 0.15.0. diff --git a/examples/path.rs b/examples/path.rs index 62d9eaa..93f8530 100644 --- a/examples/path.rs +++ b/examples/path.rs @@ -9,9 +9,7 @@ fn main() { } fn setup_system(mut commands: Commands) { - let shape = PathBuilder::new() - .fill(RED) - .stroke((BLACK, 10.0)) + let path = ShapePath::new() .move_to(Vec2::new(0., 0.)) .cubic_bezier_to( Vec2::new(70., 70.), @@ -23,9 +21,14 @@ fn setup_system(mut commands: Commands) { Vec2::new(-70., 70.), Vec2::new(0., 0.), ) - .close() - .build(); + .close(); commands.spawn((Camera2d, Msaa::Sample4)); - commands.spawn((shape, Transform::from_xyz(0., 75., 0.))); + commands.spawn(( + ShapeBuilder::with(&path) + .fill(RED) + .stroke((BLACK, 10.0)) + .build(), + Transform::from_xyz(0., 75., 0.), + )); } diff --git a/src/entity.rs b/src/entity.rs index ed17e7b..0c0c913 100644 --- a/src/entity.rs +++ b/src/entity.rs @@ -2,6 +2,7 @@ #![expect(deprecated)] use bevy::prelude::*; +use lyon_algorithms::path::Builder; use lyon_tessellation::{self as tess}; use crate::{ @@ -69,8 +70,8 @@ impl Shape { } } -impl Geometry for Shape { - fn add_geometry(&self, b: &mut tess::path::path::Builder) { +impl Geometry for Shape { + fn add_geometry(&self, b: &mut Builder) { b.extend_from_paths(&[self.path.as_slice()]); } } diff --git a/src/geometry.rs b/src/geometry.rs index c1a9c03..7bed94e 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -1,5 +1,6 @@ //! Types for defining and using geometries. +use lyon_algorithms::path::{builder::WithSvg, BuilderImpl}; use lyon_tessellation::path::path::Builder; use crate::{ @@ -38,7 +39,7 @@ use crate::{ /// } /// /// // Finally, implement the `add_geometry` method. -/// impl Geometry for Rectangle { +/// impl Geometry for Rectangle { /// fn add_geometry(&self, b: &mut Builder) { /// b.add_rectangle( /// &Box2D::new(Point::zero(), Point::new(self.width, self.height)), @@ -47,38 +48,38 @@ use crate::{ /// } /// } /// ``` -pub trait Geometry { +pub trait Geometry { /// Adds the geometry of the shape to the given Lyon path `Builder`. - fn add_geometry(&self, b: &mut Builder); + fn add_geometry(&self, b: &mut GenericBuilder); } /// Provides basic functionality common /// to [`ShapeBuilder`] and [`ReadyShapeBuilder`]. -pub trait ShapeBuilderBase { +pub trait ShapeBuilderBase { /// Adds a `Geometry` to the builder. #[must_use] - fn add(self, shape: &impl Geometry) -> Self; + fn add(self, shape: &impl Geometry) -> Self; /// Sets the fill mode of the builder. #[must_use] - fn fill(self, fill: impl Into) -> ReadyShapeBuilder; + fn fill(self, fill: impl Into) -> ReadyShapeBuilder; /// Sets the stroke mode of the builder. #[must_use] - fn stroke(self, stroke: impl Into) -> ReadyShapeBuilder; + fn stroke(self, stroke: impl Into) -> ReadyShapeBuilder; } /// Provides methods for building a shape. #[derive(Default, Clone)] -pub struct ShapeBuilder(Builder); +pub struct ShapeBuilder(GenericBuilder); -impl ShapeBuilderBase for ShapeBuilder { - fn add(mut self, shape: &impl Geometry) -> Self { +impl ShapeBuilderBase for ShapeBuilder { + fn add(mut self, shape: &impl Geometry) -> Self { shape.add_geometry(&mut self.0); self } - fn fill(self, fill: impl Into) -> ReadyShapeBuilder { + fn fill(self, fill: impl Into) -> ReadyShapeBuilder { ReadyShapeBuilder { builder: self.0, fill: Some(fill.into()), @@ -86,7 +87,7 @@ impl ShapeBuilderBase for ShapeBuilder { } } - fn stroke(self, stroke: impl Into) -> ReadyShapeBuilder { + fn stroke(self, stroke: impl Into) -> ReadyShapeBuilder { ReadyShapeBuilder { builder: self.0, fill: None, @@ -95,16 +96,19 @@ impl ShapeBuilderBase for ShapeBuilder { } } -impl ShapeBuilder { +impl ShapeBuilder +where + GenericBuilder: LyonPathBuilderExt, +{ /// Constructs a new `ShapeBuilder`. #[must_use] pub fn new() -> Self { - Self(Builder::new()) + Self(GenericBuilder::new()) } /// Constructs a new `ShapeBuilder` with an initial `Geometry`. #[must_use] - pub fn with(geometry: &impl Geometry) -> Self { + pub fn with(geometry: &impl Geometry) -> Self { Self::new().add(geometry) } } @@ -113,36 +117,67 @@ impl ShapeBuilder { /// /// This struct can only be obtained by using [`ShapeBuilder`]. #[derive(Clone)] -pub struct ReadyShapeBuilder { - pub(crate) builder: Builder, +pub struct ReadyShapeBuilder { + pub(crate) builder: GenericBuilder, pub(crate) fill: Option, pub(crate) stroke: Option, } -impl ShapeBuilderBase for ReadyShapeBuilder { - fn add(mut self, shape: &impl Geometry) -> Self { +impl ShapeBuilderBase for ReadyShapeBuilder { + fn add(mut self, shape: &impl Geometry) -> Self { shape.add_geometry(&mut self.builder); self } - fn fill(self, fill: impl Into) -> ReadyShapeBuilder { + fn fill(self, fill: impl Into) -> Self { Self { fill: Some(fill.into()), ..self } } - fn stroke(self, stroke: impl Into) -> ReadyShapeBuilder { + fn stroke(self, stroke: impl Into) -> Self { Self { stroke: Some(stroke.into()), ..self } } } -impl ReadyShapeBuilder { + +/// Generalizes the implementation of the `build` method +/// over multiple Lyon builders. +pub trait ReadyShapeBuilderTrait { /// Builds a `Shape` according to the builder's settings. - #[allow(clippy::must_use_candidate)] - pub fn build(self) -> Shape { + fn build(self) -> Shape; +} + +impl ReadyShapeBuilderTrait for ReadyShapeBuilder { + fn build(self) -> Shape { + Shape::new(self.builder.build(), self.fill, self.stroke) + } +} + +impl ReadyShapeBuilderTrait for ReadyShapeBuilder> { + fn build(self) -> Shape { Shape::new(self.builder.build(), self.fill, self.stroke) } } + +/// Extends `lyon` path builders with analogous methods +/// under the same interface. +pub trait LyonPathBuilderExt { + /// Creates a new path builder. + fn new() -> Self; +} + +impl LyonPathBuilderExt for Builder { + fn new() -> Self { + Self::new() + } +} + +impl LyonPathBuilderExt for WithSvg { + fn new() -> Self { + Builder::new().with_svg() + } +} diff --git a/src/lib.rs b/src/lib.rs index 989ce07..de8e9bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,8 +43,8 @@ pub mod prelude { pub use crate::{ draw::{Fill, Stroke}, entity::Shape, - geometry::{Geometry, ShapeBuilder, ShapeBuilderBase}, - path::PathBuilder, + geometry::{Geometry, ReadyShapeBuilderTrait, ShapeBuilder, ShapeBuilderBase}, + path::ShapePath, plugin::ShapePlugin, shapes::{self, BorderRadii, RectangleOrigin, RegularPolygon, RegularPolygonFeature}, }; diff --git a/src/path.rs b/src/path.rs index 904b43b..b3ddfb9 100644 --- a/src/path.rs +++ b/src/path.rs @@ -3,120 +3,137 @@ use bevy::math::Vec2; use lyon_tessellation::{ geom::Angle, - path::{ - builder::WithSvg, - path::{Builder, BuilderImpl}, - }, + path::{builder::WithSvg, path::BuilderImpl}, }; use crate::{ - draw::{Fill, Stroke}, - entity::Shape, + prelude::Geometry, utils::{ToPoint, ToVector}, }; -/// A SVG-like path builder. -pub struct PathBuilder { - builder: WithSvg, - fill: Option, - stroke: Option, +/// Describes an atomic action defining a [`Path`]. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, PartialEq)] +enum Action { + MoveTo(Vec2), + LineTo(Vec2), + QuadraticBezierTo { + ctrl: Vec2, + to: Vec2, + }, + CubicBezierTo { + ctrl1: Vec2, + ctrl2: Vec2, + to: Vec2, + }, + Arc { + center: Vec2, + radii: Vec2, + sweep_angle: f32, + x_rotation: f32, + }, + Close, } -impl PathBuilder { - /// Returns a new, empty `PathBuilder`. - #[must_use] - pub fn new() -> Self { - Self { - builder: Builder::new().with_svg(), - fill: None, - stroke: None, - } - } - - /// Returns a finalized [`Shape`]. - #[must_use] - pub fn build(self) -> Shape { - Shape::new(self.builder.build(), self.fill, self.stroke) - } - - /// Sets the fill mode of the path. - #[must_use] - pub fn fill(self, fill: impl Into) -> Self { - Self { - fill: Some(fill.into()), - ..self - } - } +#[derive(Default, Clone, PartialEq, Debug)] +/// A custom, SVG-like `Path`. +pub struct ShapePath { + actions: Vec, +} - /// Sets the stroke mode of the path. +impl ShapePath { + /// Creates a new `Path`. #[must_use] - pub fn stroke(self, stroke: impl Into) -> Self { - Self { - stroke: Some(stroke.into()), - ..self - } + pub fn new() -> Self { + Self::default() } /// Moves the current point to the given position. #[must_use] pub fn move_to(mut self, to: Vec2) -> Self { - self.builder.move_to(to.to_point()); + self.actions.push(Action::MoveTo(to)); self } /// Adds to the path a line from the current position to the given one. #[must_use] pub fn line_to(mut self, to: Vec2) -> Self { - self.builder.line_to(to.to_point()); - self - } - - /// Closes the shape, adding to the path a line from the current position to - /// the starting location. - #[must_use] - pub fn close(mut self) -> Self { - self.builder.close(); + self.actions.push(Action::LineTo(to)); self } /// Adds a quadratic bezier to the path. #[must_use] pub fn quadratic_bezier_to(mut self, ctrl: Vec2, to: Vec2) -> Self { - self.builder - .quadratic_bezier_to(ctrl.to_point(), to.to_point()); + self.actions.push(Action::QuadraticBezierTo { ctrl, to }); self } /// Adds a cubic bezier to the path. #[must_use] pub fn cubic_bezier_to(mut self, ctrl1: Vec2, ctrl2: Vec2, to: Vec2) -> Self { - self.builder - .cubic_bezier_to(ctrl1.to_point(), ctrl2.to_point(), to.to_point()); + self.actions + .push(Action::CubicBezierTo { ctrl1, ctrl2, to }); self } /// Adds an arc to the path. #[must_use] pub fn arc(mut self, center: Vec2, radii: Vec2, sweep_angle: f32, x_rotation: f32) -> Self { - self.builder.arc( - center.to_point(), - radii.to_vector(), - Angle::radians(sweep_angle), - Angle::radians(x_rotation), - ); + self.actions.push(Action::Arc { + center, + radii, + sweep_angle, + x_rotation, + }); self } - /// Returns the path's current position. - #[allow(clippy::must_use_candidate)] - pub fn current_position(&self) -> Vec2 { - let p = self.builder.current_position(); - Vec2::new(p.x, p.y) + /// Closes the shape, adding to the path a line from the current position to + /// the starting location. + #[must_use] + pub fn close(mut self) -> Self { + self.actions.push(Action::Close); + self } } -impl Default for PathBuilder { - fn default() -> Self { - Self::new() +impl Geometry> for ShapePath { + fn add_geometry(&self, b: &mut WithSvg) { + for action in &self.actions { + match_action(*action, b); + } } } + +fn match_action(action: Action, b: &mut WithSvg) { + match action { + Action::MoveTo(to) => { + b.move_to(to.to_point()); + } + Action::LineTo(to) => { + b.line_to(to.to_point()); + } + Action::QuadraticBezierTo { ctrl, to } => { + b.quadratic_bezier_to(ctrl.to_point(), to.to_point()); + } + Action::CubicBezierTo { ctrl1, ctrl2, to } => { + b.cubic_bezier_to(ctrl1.to_point(), ctrl2.to_point(), to.to_point()); + } + Action::Arc { + center, + radii, + sweep_angle, + x_rotation, + } => b.arc( + center.to_point(), + radii.to_vector(), + Angle::radians(sweep_angle), + Angle::radians(x_rotation), + ), + + Action::Close => { + b.close(); + } + }; +} diff --git a/src/shapes.rs b/src/shapes.rs index 3976cad..693f3e4 100644 --- a/src/shapes.rs +++ b/src/shapes.rs @@ -136,7 +136,7 @@ impl Default for Rectangle { } } -impl Geometry for Rectangle { +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), @@ -174,7 +174,7 @@ impl Default for Circle { } } -impl Geometry for Circle { +impl Geometry for Circle { fn add_geometry(&self, b: &mut Builder) { b.add_circle(self.center.to_point(), self.radius, Winding::Positive); } @@ -196,7 +196,7 @@ impl Default for Ellipse { } } -impl Geometry for Ellipse { +impl Geometry for Ellipse { fn add_geometry(&self, b: &mut Builder) { b.add_ellipse( self.center.to_point(), @@ -223,7 +223,7 @@ impl Default for Polygon { } } -impl Geometry for Polygon { +impl Geometry for Polygon { fn add_geometry(&self, b: &mut Builder) { let points = self .points @@ -257,7 +257,7 @@ impl Default for RoundedPolygon { } } -impl Geometry for RoundedPolygon { +impl Geometry for RoundedPolygon { fn add_geometry(&self, b: &mut Builder) { let points = self .points @@ -320,7 +320,7 @@ impl Default for RegularPolygon { } } -impl Geometry for RegularPolygon { +impl Geometry for RegularPolygon { fn add_geometry(&self, b: &mut Builder) { // -- Implementation details **PLEASE KEEP UPDATED** -- // - `step`: angle between two vertices. @@ -357,7 +357,7 @@ impl Geometry for RegularPolygon { #[derive(Debug, Clone, Copy, PartialEq)] pub struct Line(pub Vec2, pub Vec2); -impl Geometry for Line { +impl Geometry for Line { fn add_geometry(&self, b: &mut Builder) { b.add_polygon(LyonPolygon { points: &[self.0.to_point(), self.1.to_point()], @@ -411,7 +411,7 @@ fn get_point_after_offset(x: f64, y: f64, offset_x: f32, offset_y: f32) -> Point 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 { +impl Geometry for SvgPathShape { #[allow(clippy::too_many_lines)] fn add_geometry(&self, b: &mut Builder) { let builder = Builder::new();