From fac9d218ad3c955439c4781a57be7c5d6d9b9041 Mon Sep 17 00:00:00 2001 From: Dietrich Date: Tue, 6 Dec 2022 14:04:44 +0100 Subject: [PATCH] Initial turtle-lib commit Working so far: * display a window * display a turtle * go forward --- .gitignore | 2 + Cargo.toml | 15 ++ src/commands.rs | 161 +++++++++++++++++++++ src/debug.rs | 12 ++ src/drawing.rs | 15 ++ src/drawing/animation.rs | 204 +++++++++++++++++++++++++++ src/drawing/animation/circle_lens.rs | 51 +++++++ src/drawing/animation/line_lens.rs | 24 ++++ src/drawing/line_segments.rs | 82 +++++++++++ src/drawing/run_step.rs | 58 ++++++++ src/events.rs | 3 + src/general.rs | 9 ++ src/general/angle.rs | 195 +++++++++++++++++++++++++ src/general/length.rs | 6 + src/lib.rs | 118 ++++++++++++++++ src/shapes.rs | 13 ++ src/shapes/turtle.rs | 46 ++++++ src/state.rs | 69 +++++++++ src/turtle_bundle.rs | 75 ++++++++++ 19 files changed, 1158 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/commands.rs create mode 100644 src/debug.rs create mode 100644 src/drawing.rs create mode 100644 src/drawing/animation.rs create mode 100644 src/drawing/animation/circle_lens.rs create mode 100644 src/drawing/animation/line_lens.rs create mode 100644 src/drawing/line_segments.rs create mode 100644 src/drawing/run_step.rs create mode 100644 src/events.rs create mode 100644 src/general.rs create mode 100644 src/general/angle.rs create mode 100644 src/general/length.rs create mode 100644 src/lib.rs create mode 100644 src/shapes.rs create mode 100644 src/shapes/turtle.rs create mode 100644 src/state.rs create mode 100644 src/turtle_bundle.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8d0ed16 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "turtle-lib" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +bevy = { version = "0.9" } +bevy_prototype_lyon = {version="0.7"} +bevy-inspector-egui = "0.14" +num-traits = "0.2" +bevy_tweening = {version="0.6"} \ No newline at end of file diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..ae5a45d --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,161 @@ +use bevy::prelude::Component; +use bevy_inspector_egui::Inspectable; + +use crate::{ + drawing::{ + self, + animation::{ + draw_straight_segment, move_straight_segment, ToAnimationSegment, + TurtleAnimationSegment, + }, + TurtleGraphElement, + }, + general::{angle::Angle, length::Length, Coordinate, Precision}, + state::TurtleState, +}; +/** + * All the possibilities to draw something with turtle. All the commands can get the position, heading, + * color and fill_color from the turtles state. + */ +#[derive(Component, Inspectable)] +pub enum MoveCommand { + Forward(Length), + Backward(Length), + Circle { + radius: Length, + angle: Angle, + }, + Goto(Coordinate), +} + +impl Default for MoveCommand { + fn default() -> Self { + Self::Forward(Length(100.)) + } +} +/// Different ways to drop breadcrumbs on the way like a dot or a stamp of the turtles shape. + +#[derive(Component, Inspectable, Default)] +pub enum Breadcrumb { + Dot, + #[default] + Stamp, +} + +/// Different ways that change the orientation of the turtle. +#[derive(Component, Inspectable)] +pub enum OrientationCommand { + Left(Angle), + Right(Angle), + SetHeading, + LookAt(Coordinate), +} + +impl Default for OrientationCommand { + fn default() -> Self { + Self::Right(Default::default()) + } +} + +/// A combination of all commands that can be used while drawing. +#[derive(Component, Inspectable)] +pub enum DrawElement { + Draw(MoveCommand), + Move(MoveCommand), + Orient(OrientationCommand), + Drip(Breadcrumb), +} + +impl Default for DrawElement { + fn default() -> Self { + Self::Draw(Default::default()) + } +} +impl ToAnimationSegment for DrawElement { + fn to_draw_segment( + &self, + state: &mut TurtleState, + ) -> crate::drawing::animation::TurtleAnimationSegment { + match self { + DrawElement::Draw(e) => match e { + MoveCommand::Forward(length) => draw_straight_segment(state, length.0), + MoveCommand::Backward(length) => draw_straight_segment(state, -length.0), + MoveCommand::Circle { radius, angle } => todo!(), + MoveCommand::Goto(coord) => todo!(), + }, + DrawElement::Move(e) => match e { + MoveCommand::Forward(length) => move_straight_segment(state, length.0), + MoveCommand::Backward(length) => move_straight_segment(state, -length.0), + MoveCommand::Circle { radius, angle } => todo!(), + MoveCommand::Goto(coord) => todo!(), + }, + DrawElement::Orient(_) => todo!(), + DrawElement::Drip(_) => todo!(), + } + } +} + +#[derive(Component, Inspectable)] +pub enum TurtleSegment { + Single(DrawElement), + Outline(Vec), + Filled(Vec), +} + +impl Default for TurtleSegment { + fn default() -> Self { + Self::Single(Default::default()) + } +} +impl ToAnimationSegment for TurtleSegment { + fn to_draw_segment( + &self, + state: &mut TurtleState, + ) -> crate::drawing::animation::TurtleAnimationSegment { + match self { + Self::Single(e) => e.to_draw_segment(state), + Self::Outline(_) => todo!(), + Self::Filled(_) => todo!(), + } + } +} +#[derive(Component, Inspectable)] +pub struct TurtleCommands { + animation_state: usize, + commands: Vec, + lines: Vec, + state: TurtleState, +} + +impl TurtleCommands { + pub fn new(commands: Vec) -> Self { + let mut state = TurtleState::default(); + state.set_speed(200); + Self { + animation_state: 0, + commands, + lines: vec![], + state, + } + } + pub fn push(&mut self, segment: TurtleSegment) { + self.commands.push(segment) + } +} + +impl Iterator for TurtleCommands { + type Item = TurtleAnimationSegment; + + fn next(&mut self) -> Option { + let index = self.animation_state; + let next_index = index + 1; + + if let Some(command) = self.commands.get(self.animation_state) { + let res = command.to_draw_segment(&mut self.state); + self.animation_state = next_index; + Some(res) + } else { + None + } + } +} diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..214360c --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,12 @@ +use bevy::prelude::Plugin; +use bevy_inspector_egui::WorldInspectorPlugin; + +pub struct DebugPlugin; + +impl Plugin for DebugPlugin { + fn build(&self, app: &mut bevy::prelude::App) { + if cfg!(debug_assertions) { + app.add_plugin(WorldInspectorPlugin::new()); + } + } +} diff --git a/src/drawing.rs b/src/drawing.rs new file mode 100644 index 0000000..1eda672 --- /dev/null +++ b/src/drawing.rs @@ -0,0 +1,15 @@ +use bevy_inspector_egui::Inspectable; + +pub use self::line_segments::{TurtleDrawCircle, TurtleDrawLine}; + +pub mod animation; +mod line_segments; +pub(crate) mod run_step; + +#[derive(Inspectable, Default)] +pub enum TurtleGraphElement { + TurtleLine(TurtleDrawLine), + TurtleCircle(TurtleDrawCircle), + #[default] + Noop, +} diff --git a/src/drawing/animation.rs b/src/drawing/animation.rs new file mode 100644 index 0000000..682f8dc --- /dev/null +++ b/src/drawing/animation.rs @@ -0,0 +1,204 @@ +mod circle_lens; +mod line_lens; + +use bevy::{ + prelude::{Quat, Transform, Vec2, Vec3}, + render::render_resource::encase::rts_array::Length, +}; +use bevy_prototype_lyon::prelude::Path; +use bevy_tweening::{ + lens::{TransformPositionLens, TransformRotateZLens}, + Animator, EaseFunction, RepeatCount, RepeatStrategy, Tween, +}; + +use crate::{ + general::{angle::Angle, Coordinate, Precision}, + state::TurtleState, +}; + +use self::{ + circle_lens::{CircleAnimationLens, CircleMovementLens}, + line_lens::LineAnimationLens, +}; + +use super::{TurtleDrawCircle, TurtleDrawLine, TurtleGraphElement}; + +pub struct TurtleAnimationSegment { + pub turtle_animation: Option>, + pub line_segment: Option, + pub line_animation: Option>, +} + +pub trait ToAnimationSegment { + fn to_draw_segment( + &self, + state: &mut TurtleState, + ) -> crate::drawing::animation::TurtleAnimationSegment; +} + +pub fn turtle_turn( + state: &mut TurtleState, + angle_to_turn: Angle, +) -> TurtleAnimationSegment { + let start = state.heading(); + let end = state.heading() + angle_to_turn; + let animation = Tween::new( + EaseFunction::QuadraticInOut, + state.animation_duration(), + TransformRotateZLens { + start: start.to_radians().value(), + end: end.to_radians().value(), + }, + ) + .with_completed_event(state.segment_index() as u64); + // Don't draw as the position does not change + let line = TurtleGraphElement::Noop; + // Update the state + state.set_heading(end.limit_smaller_than_full_circle()); + TurtleAnimationSegment { + turtle_animation: Some(animation), + line_segment: Some(line), + line_animation: None, + } +} + +pub fn move_straight_segment(state: &mut TurtleState, length: Precision) -> TurtleAnimationSegment { + let animation = MoveStraightTurtleAnimation::new(state, length); + + state.set_position(animation.end); + TurtleAnimationSegment { + turtle_animation: Some(animation.animation), + line_segment: None, + line_animation: None, + } +} + +pub fn draw_straight_segment(state: &mut TurtleState, length: Precision) -> TurtleAnimationSegment { + let animation = MoveStraightTurtleAnimation::new(state, length); + let line_animation = MoveStraightLineAnimation::new(state, length, &animation); + + state.set_position(animation.end); + TurtleAnimationSegment { + turtle_animation: Some(animation.animation), + line_segment: Some(TurtleGraphElement::TurtleLine(line_animation.line)), + line_animation: Some(Animator::new(line_animation.animation)), + } +} + +struct MoveStraightLineAnimation { + start: Coordinate, + end: Coordinate, + line: TurtleDrawLine, + animation: Tween, +} + +impl MoveStraightLineAnimation { + fn new( + state: &TurtleState, + length: Precision, + turtle_animation: &MoveStraightTurtleAnimation, + ) -> Self { + let line = TurtleDrawLine::new(turtle_animation.start, turtle_animation.end); + let line_animation = Tween::new( + EaseFunction::QuadraticInOut, + state.animation_duration(), + LineAnimationLens::new(turtle_animation.start, turtle_animation.end), + ) + /* .with_repeat_strategy(RepeatStrategy::MirroredRepeat) + .with_repeat_count(RepeatCount::Infinite)*/; + Self { + start: turtle_animation.start, + end: turtle_animation.end, + line, + animation: line_animation, + } + } +} + +struct MoveStraightTurtleAnimation { + start: Coordinate, + end: Coordinate, + animation: Tween, +} +impl MoveStraightTurtleAnimation { + fn new(state: &TurtleState, length: Precision) -> Self { + let start = state.position(); + let end = + state.position() + (Vec2::from_angle(state.heading().to_radians().value()) * length); + let turtle_movement_animation = Tween::new( + EaseFunction::QuadraticInOut, + state.animation_duration(), + TransformPositionLens { + start: start.extend(0.), + end: end.extend(0.), + }, + ) + .with_completed_event(state.segment_index() as u64); + Self { + start, + end, + animation: turtle_movement_animation, + } + } +} + +pub fn turtle_circle( + state: &mut TurtleState, + radius: Precision, + angle: Angle, +) -> TurtleAnimationSegment { + let radii = Vec2::ONE * radius.abs(); + let left_right = Angle::degrees(if radius >= 0. { 90. } else { -90. }); + let center = state.position() + + (Vec2::new(radius.abs(), 0.).rotate(Vec2::from_angle( + ((state.heading() + left_right).to_radians()).value(), + ))); + + let turtle_movement_animation = Tween::new( + EaseFunction::QuadraticInOut, + state.animation_duration(), + CircleMovementLens { + start: Transform { + translation: state.position().extend(0.), + rotation: Quat::from_rotation_z(state.heading().to_radians().value()), + scale: Vec3::ONE, + }, + end: angle, + center, + }, + ) + .with_completed_event(state.segment_index()); + let end_pos = center + + Vec2::new(radius.abs(), 0.).rotate(Vec2::from_angle( + (state.heading() + angle - left_right).to_radians().value(), + )); + let line = /* if state.drawing { */ + TurtleGraphElement::TurtleCircle(TurtleDrawCircle::new( + center, + radii, + Angle::degrees(0.), + state.position(), + end_pos, + )) + /* } else { + TurtleGraphElement::Noop + } */; + let line_animator = Animator::new(Tween::new( + EaseFunction::QuadraticInOut, + state.animation_duration(), + CircleAnimationLens { + start_pos: state.position(), + center, + radii, + start: Angle::degrees(0.), + end: angle, + }, + )); + state.set_position(end_pos); + state.set_heading(state.heading() + angle); + TurtleAnimationSegment { + turtle_animation: Some(turtle_movement_animation), + line_segment: Some(line), + line_animation: Some(line_animator), + } +} diff --git a/src/drawing/animation/circle_lens.rs b/src/drawing/animation/circle_lens.rs new file mode 100644 index 0000000..e38f23a --- /dev/null +++ b/src/drawing/animation/circle_lens.rs @@ -0,0 +1,51 @@ +use bevy::prelude::{Quat, Transform, Vec2}; +use bevy_prototype_lyon::prelude::{Path, PathBuilder, ShapePath}; +use bevy_tweening::Lens; + +use crate::general::{angle::Angle, Precision}; + +pub(crate) struct CircleAnimationLens { + pub start_pos: Vec2, + pub center: Vec2, + pub radii: Vec2, + pub start: Angle, + pub end: Angle, +} + +impl Lens for CircleAnimationLens { + fn lerp(&mut self, target: &mut Path, ratio: f32) { + let mut path_builder = PathBuilder::new(); + path_builder.move_to(self.start_pos); + // The center point of the radius, then the radii in x and y direction, then the angle that will be drawn, then the x_rotation ? + path_builder.arc( + self.center, + self.radii, + (self.start + ((self.end - self.start) * ratio)) + .to_radians() + .value(), + 0., + ); + let line = path_builder.build(); + *target = ShapePath::build_as(&line); + } +} + +pub(crate) struct CircleMovementLens { + pub center: Vec2, + pub start: Transform, + pub end: Angle, +} + +impl Lens for CircleMovementLens { + fn lerp(&mut self, target: &mut Transform, ratio: f32) { + let angle = self.end * ratio; + let mut rotated = self.start; + + rotated.rotate_around( + self.center.extend(0.), + Quat::from_rotation_z(angle.to_radians().value()), + ); + + *target = rotated; + } +} diff --git a/src/drawing/animation/line_lens.rs b/src/drawing/animation/line_lens.rs new file mode 100644 index 0000000..6bb8011 --- /dev/null +++ b/src/drawing/animation/line_lens.rs @@ -0,0 +1,24 @@ +use bevy::prelude::Vec2; +use bevy_prototype_lyon::{ + prelude::{Path, ShapePath}, + shapes, +}; +use bevy_tweening::Lens; + +pub(crate) struct LineAnimationLens { + start: Vec2, + end: Vec2, +} + +impl LineAnimationLens { + pub(crate) fn new(start: Vec2, end: Vec2) -> Self { + Self { start, end } + } +} + +impl Lens for LineAnimationLens { + fn lerp(&mut self, target: &mut Path, ratio: f32) { + let line = shapes::Line(self.start, self.start + ((self.end - self.start) * ratio)); + *target = ShapePath::build_as(&line); + } +} diff --git a/src/drawing/line_segments.rs b/src/drawing/line_segments.rs new file mode 100644 index 0000000..ee76338 --- /dev/null +++ b/src/drawing/line_segments.rs @@ -0,0 +1,82 @@ +use bevy::prelude::{Bundle, Color, Component, Name, Transform, Vec2}; +use bevy_inspector_egui::Inspectable; +use bevy_prototype_lyon::{ + entity::ShapeBundle, + prelude::{DrawMode, FillMode, GeometryBuilder, PathBuilder, StrokeMode}, + shapes::Line, +}; + +use crate::general::{angle::Angle, Precision}; + +#[derive(Bundle, Inspectable, Default)] +pub struct TurtleDrawLine { + #[inspectable(ignore)] + line: ShapeBundle, + name: Name, + marker: LineMarker, +} + +#[derive(Component, Default, Inspectable)] +struct LineMarker; + +impl TurtleDrawLine { + pub(crate) fn new(start: Vec2, end: Vec2) -> Self { + Self { + line: GeometryBuilder::build_as( + &Line(start, start), + DrawMode::Outlined { + fill_mode: FillMode::color(Color::MIDNIGHT_BLUE), + outline_mode: StrokeMode::new(Color::BLACK, 1.0), + }, + Transform::IDENTITY, + ), + name: Name::new(format!("Line {}-{}", start, end)), + marker: LineMarker, + } + } +} + +#[derive(Bundle, Inspectable, Default)] + +pub struct TurtleDrawCircle { + #[inspectable(ignore)] + line: ShapeBundle, + name: Name, + marker: CircleMarker, +} + +#[derive(Component, Default, Inspectable)] +struct CircleMarker; + +impl TurtleDrawCircle { + pub(crate) fn new( + center: Vec2, + radii: Vec2, + angle: Angle, + start: Vec2, + _end: Vec2, + ) -> Self { + let mut path_builder = PathBuilder::new(); + path_builder.move_to(start); + // The center point of the radius - this is responsible for the orientation of the ellipse, + // then the radii in x and y direction - this can be rotated using the x_rotation parameter, + // then the angle - the part of the circle that will be drawn like (PI/2.0) for a quarter circle, + // then the x_rotation (maybe the rotation of the radii?) + path_builder.arc(center, radii, angle.to_radians().value(), 0.); + let line = path_builder.build(); + println!("Draw Circle: {} {} {:?}", center, radii, angle); + + Self { + line: GeometryBuilder::build_as( + &line, + DrawMode::Outlined { + fill_mode: FillMode::color(Color::rgba(0., 0., 0., 0.)), + outline_mode: StrokeMode::new(Color::BLACK, 1.0), + }, + Transform::IDENTITY, + ), + name: Name::new(format!("Circle at {}, {}", center.x, center.y)), + marker: CircleMarker, + } + } +} diff --git a/src/drawing/run_step.rs b/src/drawing/run_step.rs new file mode 100644 index 0000000..b272b66 --- /dev/null +++ b/src/drawing/run_step.rs @@ -0,0 +1,58 @@ +use bevy::prelude::{Commands, Query, Transform, With}; +use bevy_tweening::Animator; + +use crate::{commands::TurtleCommands, shapes::TurtleShape}; + +use super::{animation::TurtleAnimationSegment, TurtleGraphElement}; + +pub fn run_animation_step( + commands: &mut Commands, + tcmd: &mut TurtleCommands, + turtle: &mut Query<&mut Animator, With>, +) { + loop { + match tcmd.next() { + Some(TurtleAnimationSegment { + turtle_animation: Some(turtle_animation), + line_segment: Some(graph_element_to_draw), + line_animation: Some(line_animation), + }) => { + let mut turtle = turtle.single_mut(); + turtle.set_tweenable(turtle_animation); + match graph_element_to_draw { + TurtleGraphElement::TurtleLine(line) => { + commands.spawn((line, line_animation)); + } + TurtleGraphElement::Noop => (), + TurtleGraphElement::TurtleCircle(circle) => { + commands.spawn((circle, line_animation)); + } + } + return; + } + // In case a rotation is performed the line drawing can be skipped + Some(TurtleAnimationSegment { + turtle_animation: Some(turtle_animation), + line_segment: Some(_), + line_animation: None, + })| + // In case a rotation is performed the line drawing can be skipped + Some(TurtleAnimationSegment { + turtle_animation: Some(turtle_animation), + line_segment: None, + line_animation: None, + }) => { + let mut turtle = turtle.single_mut(); + turtle.set_tweenable(turtle_animation); + return; + } + Some(_e) => { + println!("without animation"); + } + None => { + println!("nothing to draw"); + return; + } + }; + } +} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..fb0bb9b --- /dev/null +++ b/src/events.rs @@ -0,0 +1,3 @@ +use bevy::prelude::Entity; + +pub struct DrawingStartedEvent(pub Entity); diff --git a/src/general.rs b/src/general.rs new file mode 100644 index 0000000..335289a --- /dev/null +++ b/src/general.rs @@ -0,0 +1,9 @@ +use bevy::prelude::Vec2; + +pub mod angle; +pub mod length; + +pub type Precision = f32; +pub type Coordinate = Vec2; +pub type Visibility = bool; +pub type Speed = u32; diff --git a/src/general/angle.rs b/src/general/angle.rs new file mode 100644 index 0000000..3d98c48 --- /dev/null +++ b/src/general/angle.rs @@ -0,0 +1,195 @@ +use bevy_inspector_egui::Inspectable; +use std::{ + f32::consts::PI, + ops::{Add, Div, Mul, Neg, Rem, Sub}, +}; + +use super::Precision; + +#[derive(Inspectable, Copy, Clone, Debug, PartialEq, Eq)] +pub enum AngleUnit { + Degrees(T), + Radians(T), +} + +impl Default for AngleUnit { + fn default() -> Self { + Self::Degrees(Default::default()) + } +} + +#[derive(Inspectable, Copy, Default, Clone, Debug, PartialEq, Eq)] +pub struct Angle { + value: AngleUnit, +} + +impl> Rem for Angle { + type Output = Self; + + fn rem(self, rhs: T) -> Self::Output { + match self.value { + AngleUnit::Degrees(v) => Self::Output::degrees(v % rhs), + AngleUnit::Radians(v) => Self::Output::radians(v % rhs), + } + } +} + +impl> Mul for Angle { + type Output = Self; + + fn mul(self, rhs: T) -> Self::Output { + match self.value { + AngleUnit::Degrees(v) => Self::Output::degrees(v * rhs), + AngleUnit::Radians(v) => Self::Output::radians(v * rhs), + } + } +} + +impl Angle { + pub fn limit_smaller_than_full_circle(self) -> Self { + match self.value { + AngleUnit::Degrees(v) => Self { + value: AngleUnit::Degrees(v % 360.), + }, + AngleUnit::Radians(v) => Self { + value: AngleUnit::Radians(v % (2. * PI)), + }, + } + } +} +impl> Div for Angle { + type Output = Self; + + fn div(self, rhs: T) -> Self::Output { + match self.value { + AngleUnit::Degrees(v) => Self::Output::degrees(v / rhs), + AngleUnit::Radians(v) => Self::Output::radians(v / rhs), + } + } +} + +impl> Neg for Angle { + type Output = Self; + + fn neg(self) -> Self::Output { + match self.value { + AngleUnit::Degrees(v) => Self::Output::degrees(-v), + AngleUnit::Radians(v) => Self::Output::radians(-v), + } + } +} + +impl> Neg for &Angle { + type Output = Angle; + + fn neg(self) -> Self::Output { + match self.value.clone() { + AngleUnit::Degrees(v) => Self::Output::degrees(-v), + AngleUnit::Radians(v) => Self::Output::radians(-v), + } + } +} + +impl Angle { + pub fn degrees(value: T) -> Angle { + Self { + value: AngleUnit::Degrees(value), + } + } + pub fn radians(value: T) -> Angle { + Self { + value: AngleUnit::Radians(value), + } + } + pub fn value(&self) -> T { + match self.value.clone() { + AngleUnit::Degrees(v) => v, + AngleUnit::Radians(v) => v, + } + } +} + +impl Angle { + pub fn to_radians(self) -> Self { + match self.value { + AngleUnit::Degrees(v) => Self { + value: AngleUnit::Radians(v.to_radians()), + }, + AngleUnit::Radians(_) => self, + } + } + pub fn to_degrees(self) -> Self { + match self.value { + AngleUnit::Degrees(_) => self, + AngleUnit::Radians(v) => Self { + value: AngleUnit::Degrees(v.to_degrees()), + }, + } + } +} + +impl + Default + num_traits::float::Float> Add for Angle { + type Output = Angle; + + fn add(self, rhs: Self) -> Self::Output { + match (self.value, rhs.value) { + (AngleUnit::Degrees(v), AngleUnit::Degrees(o)) => Self::Output { + value: AngleUnit::Degrees(v + o), + }, + (AngleUnit::Degrees(v), AngleUnit::Radians(o)) => Self::Output { + value: AngleUnit::Radians(v.to_radians() + o), + }, + (AngleUnit::Radians(v), AngleUnit::Degrees(o)) => Self::Output { + value: AngleUnit::Radians(v + o.to_radians()), + }, + (AngleUnit::Radians(v), AngleUnit::Radians(o)) => Self::Output { + value: AngleUnit::Radians(v + o), + }, + } + } +} + +impl + Default + num_traits::float::Float> Sub for Angle { + type Output = Angle; + + fn sub(self, rhs: Self) -> Self::Output { + match (self.value, rhs.value) { + (AngleUnit::Degrees(v), AngleUnit::Degrees(o)) => Self::Output { + value: AngleUnit::Degrees(v - o), + }, + (AngleUnit::Degrees(v), AngleUnit::Radians(o)) => Self::Output { + value: AngleUnit::Radians(v.to_radians() - o), + }, + (AngleUnit::Radians(v), AngleUnit::Degrees(o)) => Self::Output { + value: AngleUnit::Radians(v - o.to_radians()), + }, + (AngleUnit::Radians(v), AngleUnit::Radians(o)) => Self::Output { + value: AngleUnit::Radians(v - o), + }, + } + } +} + +#[test] +fn convert_to_radians() { + let radi = Angle::radians(30f32.to_radians()); + let degr = Angle::degrees(30f32); + let converted = degr.to_radians(); + assert_eq!(radi, converted) +} +#[test] +fn sum_degrees() { + let fst = Angle::degrees(30f32); + let snd = Angle::degrees(30f32); + let sum = fst + snd; + assert!((sum.value() - 60f32).abs() < 0.0001); + assert!((sum.to_radians().value() - 60f32.to_radians()).abs() < 0.0001); +} +#[test] +fn sum_mixed() { + let fst = Angle::degrees(30f32); + let snd = Angle::radians(30f32.to_radians()); + let sum = fst + snd; + assert!((sum.to_degrees().value() - 60f32).abs() < 0.0001); + assert!((sum.to_radians().value() - 60f32.to_radians()).abs() < 0.0001); +} diff --git a/src/general/length.rs b/src/general/length.rs new file mode 100644 index 0000000..8ac332a --- /dev/null +++ b/src/general/length.rs @@ -0,0 +1,6 @@ +use bevy_inspector_egui::Inspectable; + +use super::Precision; + +#[derive(Inspectable, Default, Copy, Clone, Debug)] +pub struct Length(pub Precision); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..11d9f11 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,118 @@ +use std::time::Duration; + +use bevy::{core_pipeline::clear_color::ClearColorConfig, prelude::*, window::close_on_esc}; +use bevy_inspector_egui::RegisterInspectable; +use bevy_prototype_lyon::prelude::{Path, ShapePlugin}; +use bevy_tweening::{ + component_animator_system, lens::TransformScaleLens, Animator, EaseFunction, Tween, + TweenCompleted, TweeningPlugin, +}; +use events::DrawingStartedEvent; +use shapes::{TurtleColors, TurtleShape}; +use turtle_bundle::{AnimatedTurtle, TurtleBundle}; + +pub use commands::TurtleCommands; + +mod commands; +mod debug; +mod drawing; +pub mod events; +mod general; +pub mod shapes; +mod state; +mod turtle_bundle; + +/** +The turtle plugin is the core of this turtle module. + +In order to facilitate the setup this plugin also inserts the `DefaultPlugins` and many other things. + +Before using any of the functions add this plugin using: +```rust + +app::new().add_plugin(turtle_lib::TurtlePlugin) +``` +*/ +pub struct TurtlePlugin; + +impl Plugin for TurtlePlugin { + fn build(&self, app: &mut App) { + app.add_plugins(DefaultPlugins.set(WindowPlugin { + window: WindowDescriptor { + title: "Immigration Game".to_string(), + width: 1200., + height: 800., + present_mode: bevy::window::PresentMode::Fifo, // vsync + ..Default::default() + }, + ..default() + })) + .add_plugin(debug::DebugPlugin) + .add_plugin(ShapePlugin) + .add_plugin(TweeningPlugin) + .add_event::() + .add_startup_system(setup) + .add_system(keypresses) + .add_system(component_animator_system::) + .add_system(close_on_esc) + .add_system(draw_lines) + .register_inspectable::() + .register_inspectable::(); + } +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2dBundle { + camera_2d: Camera2d { + clear_color: ClearColorConfig::Custom(Color::BEIGE), + }, + ..default() + }); +} + +pub fn get_a_turtle() -> AnimatedTurtle { + let animator = Animator::new(Tween::new( + EaseFunction::QuadraticInOut, + Duration::from_millis(3000), + TransformScaleLens { + start: Vec3::ZERO, + end: Vec3::ONE * 1.3, + }, + )); + let turtle_bundle = TurtleBundle::default(); + AnimatedTurtle { + animator, + turtle_bundle, + turtle_shape: TurtleShape, + } +} + +fn keypresses( + mut commands: Commands, + keys: Res>, + mut tcmd: Query<(Entity, &mut TurtleCommands)>, + mut turtle: Query<&mut Animator, With>, + mut ev_start: EventWriter, +) { + if keys.just_pressed(KeyCode::W) { + for (entity, mut tcmd) in tcmd.iter_mut() { + crate::drawing::run_step::run_animation_step(&mut commands, &mut tcmd, &mut turtle); + ev_start.send(DrawingStartedEvent(entity)) + } + } +} + +fn draw_lines( + mut commands: Commands, + mut tcmd: Query<&mut TurtleCommands>, + mut turtle: Query<&mut Animator, With>, + mut query_event: EventReader, // TODO: howto attach only to the right event? +) { + for _ev in query_event.iter() { + if let Ok(mut tcmd) = tcmd.get_single_mut() { + crate::drawing::run_step::run_animation_step(&mut commands, &mut tcmd, &mut turtle) + } else { + println!("Failed to get the turtle") + } + } +} diff --git a/src/shapes.rs b/src/shapes.rs new file mode 100644 index 0000000..d82fdfd --- /dev/null +++ b/src/shapes.rs @@ -0,0 +1,13 @@ +mod turtle; +use bevy::prelude::{Color, Component}; +use bevy_inspector_egui::Inspectable; +pub use turtle::turtle; + +#[derive(Clone, Component, Inspectable)] +pub struct TurtleShape; + +#[derive(Clone, Component, Inspectable, Default)] +pub struct TurtleColors { + color: Color, + fill_color: Color, +} diff --git a/src/shapes/turtle.rs b/src/shapes/turtle.rs new file mode 100644 index 0000000..40fd988 --- /dev/null +++ b/src/shapes/turtle.rs @@ -0,0 +1,46 @@ +use std::f32::consts::PI; + +use bevy::prelude::Vec2; +use bevy_prototype_lyon::prelude::{Path, PathBuilder}; + +use crate::general::Precision; + +pub fn turtle() -> Path { + let polygon: &[[Precision; 2]; 23] = &[ + [-2.5, 14.0], + [-1.25, 10.0], + [-4.0, 7.0], + [-7.0, 9.0], + [-9.0, 8.0], + [-6.0, 5.0], + [-7.0, 1.0], + [-5.0, -3.0], + [-8.0, -6.0], + [-6.0, -8.0], + [-4.0, -5.0], + [0.0, -7.0], + [4.0, -5.0], + [6.0, -8.0], + [8.0, -6.0], + [5.0, -3.0], + [7.0, 1.0], + [6.0, 5.0], + [9.0, 8.0], + [7.0, 9.0], + [4.0, 7.0], + [1.25, 10.0], + [2.5, 14.0], + ]; + let mut turtle_path = PathBuilder::new(); + turtle_path.line_to(Vec2::new(1.0, 1.0)); + turtle_path.line_to(Vec2::new(-1.0, 1.0)); + turtle_path.line_to(Vec2::new(-1.0, -1.0)); + turtle_path.line_to(Vec2::new(1.0, -1.0)); + turtle_path.close(); + turtle_path.move_to(Vec2::new(0.0, 16.0).rotate(Vec2::from_angle(-PI / 2.))); + for coord in polygon { + turtle_path.line_to(Vec2::from_array(*coord).rotate(Vec2::from_angle(-PI / 2.))); + } + turtle_path.close(); + turtle_path.build() +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..29b715e --- /dev/null +++ b/src/state.rs @@ -0,0 +1,69 @@ +use std::{cmp::max, time::Duration}; + +use bevy::prelude::{Color, Component, Transform}; +use bevy_inspector_egui::Inspectable; + +use crate::{ + commands::TurtleSegment, + general::{angle::Angle, Coordinate, Precision, Speed, Visibility}, + shapes::TurtleColors, +}; + +/// Describing the full state of a turtle. +#[derive(Component, Inspectable, Default)] +pub struct TurtleState { + drawing: Vec, + position: Coordinate, + heading: Angle, + colors: TurtleColors, + visible: Visibility, + shape_transform: Transform, + speed: Speed, + segment_index: u64, +} + +impl TurtleState { + pub fn add_segment(&mut self, seg: TurtleSegment) { + self.drawing.push(seg); + } +} + +impl TurtleState { + pub fn segment_index(&self) -> u64 { + self.segment_index + } + pub fn heading(&self) -> Angle { + self.heading + } + pub fn position(&self) -> Coordinate { + self.position + } + pub fn speed(&self) -> Speed { + self.speed + } + /// The duration of animations calculated from the speed. + pub fn animation_duration(&self) -> Duration { + Duration::from_millis(self.speed() as u64) + } + pub fn shape_transform(&self) -> Transform { + self.shape_transform + } +} +impl TurtleState { + pub fn set_heading(&mut self, angle: Angle) -> &mut Self { + self.heading = angle; + self + } + pub fn set_position(&mut self, position: Coordinate) -> &mut Self { + self.position = position; + self + } + pub fn set_speed(&mut self, speed: Speed) -> &mut Self { + self.speed = max(speed, 1); + self + } + pub fn set_shape_transform(&mut self, transform: Transform) -> &mut Self { + self.shape_transform = transform; + self + } +} diff --git a/src/turtle_bundle.rs b/src/turtle_bundle.rs new file mode 100644 index 0000000..9f8ee99 --- /dev/null +++ b/src/turtle_bundle.rs @@ -0,0 +1,75 @@ +use std::ops::{Deref, DerefMut}; + +use bevy::prelude::{Bundle, Color, Name, Transform}; +use bevy_prototype_lyon::{ + entity::ShapeBundle, + prelude::{DrawMode, FillMode, GeometryBuilder, StrokeMode}, +}; + +use crate::{ + commands::{DrawElement, MoveCommand, TurtleCommands, TurtleSegment}, + general::length::Length, + shapes::{self, TurtleColors}, +}; + +#[derive(Bundle)] +pub struct TurtleBundle { + colors: TurtleColors, + pub commands: TurtleCommands, + name: Name, + shape: ShapeBundle, +} + +impl Default for TurtleBundle { + fn default() -> Self { + Self { + colors: TurtleColors::default(), + commands: TurtleCommands::new(vec![]), + name: Name::new("Turtle"), + shape: GeometryBuilder::build_as( + &shapes::turtle(), + DrawMode::Outlined { + fill_mode: FillMode::color(Color::MIDNIGHT_BLUE), + outline_mode: StrokeMode::new(Color::BLACK, 1.0), + }, + Transform::IDENTITY, + ), + } + } +} + +impl TurtleBundle { + pub fn set_commands(&mut self, commands: Vec) { + self.commands = TurtleCommands::new(commands); + } +} + +impl TurtleBundle { + pub fn forward(&mut self, len: f32) -> &mut Self { + self.commands.push(TurtleSegment::Single(DrawElement::Draw( + MoveCommand::Forward(Length(len)), + ))); + self + } +} + +#[derive(Bundle)] +pub struct AnimatedTurtle { + pub animator: bevy_tweening::Animator, + pub turtle_bundle: TurtleBundle, + pub turtle_shape: shapes::TurtleShape, +} + +impl Deref for AnimatedTurtle { + type Target = TurtleBundle; + + fn deref(&self) -> &Self::Target { + &self.turtle_bundle + } +} + +impl DerefMut for AnimatedTurtle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.turtle_bundle + } +}