improve speed to have one single point of truth.

This commit is contained in:
Franz Dietrich 2025-10-09 15:54:53 +02:00
parent cebad0459c
commit 5799d2aa07
15 changed files with 170 additions and 74 deletions

View File

@ -13,20 +13,23 @@ async fn main() {
plan.set_color(RED);
plan.set_pen_width(0.5);
plan.left(90.0);
plan.set_speed(999);
plan.circle_left(100.0, 540.0, 72); // partial circle to the left
plan.forward(150.0);
plan.set_speed(100);
plan.set_color(BLUE);
plan.circle_right(50.0, 270.0, 72); // partial circle to the right
// Set animation speed
plan.set_speed(20);
plan.forward(150.0);
plan.set_speed(700);
plan.set_color(GREEN);
plan.circle_left(50.0, 180.0, 36); // Half circle to the left
// Create turtle app with animation (speed = 100 pixels/sec)
let mut app = TurtleApp::new().with_commands(plan.build(), 100.0);
let mut app = TurtleApp::new().with_commands(plan.build());
// Main loop
loop {

View File

@ -8,14 +8,15 @@ async fn main() {
// Create a turtle plan
let mut plan = create_turtle();
// Set animation speed
plan.set_speed(50);
plan.right(45.0);
plan.forward(100.0);
plan.right(45.0);
plan.forward(100.0);
//plan.circle_left(100.0, 90.0, 72); // Full circle to the left
// Create turtle app with animation (speed = 100 pixels/sec)
let mut app = TurtleApp::new().with_commands(plan.build(), 10.0);
let mut app = TurtleApp::new().with_commands(plan.build());
// Main loop
loop {

View File

@ -23,22 +23,23 @@ async fn main() {
let mut plan = create_turtle();
// Position turtle
plan.set_speed(10);
plan.pen_up();
plan.backward(150.0);
plan.pen_down();
plan.set_speed(10000);
// Draw Koch snowflake (triangle of Koch curves)
for _ in 0..3 {
koch(4, &mut plan, 300.0);
plan.right(120.0);
plan.set_speed(1000);
}
plan.hide(); // Hide turtle when done
// Create app with animation
let mut app = TurtleApp::new().with_commands(plan.build(), 1000.0);
let mut app = TurtleApp::new().with_commands(plan.build());
loop {
clear_background(WHITE);

View File

@ -58,8 +58,8 @@ async fn main() {
nikolaus(&mut plan, 100.0);
// Create turtle app with animation (speed = 100 pixels/sec)
let mut app = TurtleApp::new().with_commands(plan.build(), 100.0);
// Create turtle app with animation
let mut app = TurtleApp::new().with_commands(plan.build());
// Main loop
loop {

View File

@ -31,8 +31,11 @@ async fn main() {
plan.shape(ShapeType::Arrow);
plan.forward(100.0);
// Set animation speed
plan.set_speed(50);
// Create turtle app with animation (speed = 100 pixels/sec for slower animation)
let mut app = TurtleApp::new().with_commands(plan.build(), 700.0);
let mut app = TurtleApp::new().with_commands(plan.build());
// Main loop
loop {

View File

@ -14,8 +14,11 @@ async fn main() {
plan.forward(100.0).right(90.0);
}
// Create turtle app with animation (speed = 100 pixels/sec)
let mut app = TurtleApp::new().with_commands(plan.build(), 100.0);
// Set animation speed
plan.set_speed(50);
// Create turtle app with animation
let mut app = TurtleApp::new().with_commands(plan.build());
// Main loop
loop {

View File

@ -18,8 +18,11 @@ async fn main() {
plan.circle_left(10.0, 72.0, 1000);
}
// Set animation speed
plan.set_speed(300);
// Create turtle app with animation (speed = 100 pixels/sec)
let mut app = TurtleApp::new().with_commands(plan.build(), 100.0);
let mut app = TurtleApp::new().with_commands(plan.build());
// Main loop
loop {

View File

@ -25,8 +25,11 @@ async fn main() {
t.pen_down();
t.circle_right(8.0, 360.0, 12);
// Set animation speed
t.set_speed(1000);
// Create turtle app with animation (speed = 100 pixels/sec)
let mut app = TurtleApp::new().with_commands(t.build(), 100.0);
let mut app = TurtleApp::new().with_commands(t.build());
// Main loop
loop {

View File

@ -1,7 +1,7 @@
//! Builder pattern traits for creating turtle command sequences
use crate::commands::{CommandQueue, TurtleCommand};
use crate::general::{Color, Precision};
use crate::general::{AnimationSpeed, Color, Precision};
use crate::shapes::{ShapeType, TurtleShape};
/// Trait for adding commands to a queue
@ -106,8 +106,11 @@ impl TurtlePlan {
}
}
pub fn set_speed(&mut self, speed: u32) -> &mut Self {
self.queue.push(TurtleCommand::SetSpeed(speed));
/// Set animation speed
/// - Values >= 999 = instant mode (no animation)
/// - Values < 999 = animated mode with specified pixels/second
pub fn set_speed(&mut self, speed: impl Into<AnimationSpeed>) -> &mut Self {
self.queue.push(TurtleCommand::SetSpeed(speed.into()));
self
}

View File

@ -1,6 +1,6 @@
//! Turtle commands and command queue
use crate::general::{Color, Coordinate, Precision};
use crate::general::{AnimationSpeed, Color, Coordinate, Precision};
use crate::shapes::TurtleShape;
/// Individual turtle commands
@ -28,7 +28,7 @@ pub enum TurtleCommand {
SetColor(Color),
SetFillColor(Option<Color>),
SetPenWidth(Precision),
SetSpeed(u32),
SetSpeed(AnimationSpeed),
SetShape(TurtleShape),
// Position

View File

@ -5,6 +5,9 @@ use crate::commands::TurtleCommand;
use crate::state::{DrawCommand, TurtleState, TurtleWorld};
use macroquad::prelude::*;
#[cfg(test)]
use crate::general::AnimationSpeed;
/// Execute a single turtle command, updating state and adding draw commands
pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world: &mut TurtleWorld) {
match command {
@ -129,17 +132,6 @@ pub fn execute_command(command: &TurtleCommand, state: &mut TurtleState, world:
}
}
/// Execute all commands immediately (no animation)
pub fn execute_all_immediate(
queue: &mut crate::commands::CommandQueue,
state: &mut TurtleState,
world: &mut TurtleWorld,
) {
while let Some(command) = queue.next() {
execute_command(command, state, world);
}
}
/// Add drawing command for a completed tween (state transition already occurred)
pub fn add_draw_for_completed_tween(
command: &TurtleCommand,
@ -228,7 +220,7 @@ mod tests {
pen_width: 1.0,
color: Color::new(0.0, 0.0, 0.0, 1.0),
fill_color: None,
speed: 100,
speed: AnimationSpeed::Animated(100.0),
visible: true,
shape: TurtleShape::turtle(),
};

View File

@ -17,8 +17,61 @@ pub type Coordinate = Vec2;
/// Visibility flag for turtle
pub type Visibility = bool;
/// Speed of animations (higher = faster, >= 999 = instant)
pub type Speed = u32;
/// Execution speed setting
/// - Instant: No animation, commands execute immediately
/// - Animated(speed): Smooth animation at specified pixels/second
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AnimationSpeed {
Instant,
Animated(f32), // pixels per second
}
impl AnimationSpeed {
/// Check if this is instant mode
pub fn is_instant(&self) -> bool {
matches!(self, AnimationSpeed::Instant)
}
/// Get the speed value (returns a high value for Instant)
pub fn value(&self) -> f32 {
match self {
AnimationSpeed::Instant => 9999.0,
AnimationSpeed::Animated(speed) => *speed,
}
}
/// Create from a raw speed value (>= 999 becomes Instant)
pub fn from_value(speed: f32) -> Self {
if speed >= 999.0 {
AnimationSpeed::Instant
} else {
AnimationSpeed::Animated(speed.max(1.0))
}
}
/// Create from a u32 value for backward compatibility
pub fn from_u32(speed: u32) -> Self {
Self::from_value(speed as f32)
}
}
impl Default for AnimationSpeed {
fn default() -> Self {
AnimationSpeed::Animated(100.0)
}
}
impl From<f32> for AnimationSpeed {
fn from(speed: f32) -> Self {
AnimationSpeed::from_value(speed)
}
}
impl From<u32> for AnimationSpeed {
fn from(speed: u32) -> Self {
AnimationSpeed::from_u32(speed)
}
}
/// Color type re-export from macroquad
pub use macroquad::color::Color;

View File

@ -13,7 +13,7 @@
//! let mut plan = create_turtle();
//! plan.forward(100.0).right(90.0).forward(100.0);
//!
//! let mut app = TurtleApp::new().with_commands(plan.build(), 100.0);
//! let mut app = TurtleApp::new().with_commands(plan.build());
//!
//! loop {
//! clear_background(WHITE);
@ -37,7 +37,7 @@ pub mod tweening;
// Re-export commonly used types
pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands};
pub use commands::{CommandQueue, TurtleCommand};
pub use general::{Angle, Color, Coordinate, Length, Precision, Speed};
pub use general::{Angle, AnimationSpeed, Color, Coordinate, Length, Precision};
pub use shapes::{ShapeType, TurtleShape};
pub use state::{DrawCommand, TurtleState, TurtleWorld};
pub use tweening::TweenController;
@ -48,7 +48,7 @@ use macroquad::prelude::*;
pub struct TurtleApp {
world: TurtleWorld,
tween_controller: Option<TweenController>,
mode: ExecutionMode,
speed: AnimationSpeed,
// Mouse panning state
is_dragging: bool,
last_mouse_pos: Option<Vec2>,
@ -56,46 +56,31 @@ pub struct TurtleApp {
zoom_level: f32,
}
enum ExecutionMode {
Immediate,
Animated,
}
impl TurtleApp {
/// Create a new turtle application with default settings
/// Create a new TurtleApp with default settings
pub fn new() -> Self {
Self {
world: TurtleWorld::new(),
tween_controller: None,
mode: ExecutionMode::Immediate,
speed: AnimationSpeed::default(),
is_dragging: false,
last_mouse_pos: None,
zoom_level: 1.0,
}
}
/// Add commands to the turtle with specified speed
/// Add commands to the turtle
///
/// Speed is controlled by SetSpeed commands in the queue.
/// Use `set_speed()` on the turtle plan to set animation speed.
/// Speed >= 999 = instant mode, speed < 999 = animated mode.
///
/// # Arguments
/// * `queue` - The command queue to execute
/// * `speed` - Animation speed in pixels/sec (>= 999.0 = instant, 0.5-999.0 = animated)
pub fn with_commands(mut self, queue: CommandQueue, speed: f32) -> Self {
if speed <= 0.5 || speed.is_infinite() || speed.is_nan() {
// Compiler error speed should be between 0.5 and 1000.0
panic!("Speed must be greater than 0.5 and less than 1000.0");
}
if speed >= 999.0 {
// Immediate mode - execute all commands instantly
self.mode = ExecutionMode::Immediate;
let mut state = TurtleState::default();
let mut queue_mut = queue;
execution::execute_all_immediate(&mut queue_mut, &mut state, &mut self.world);
self.world.turtle = state;
} else {
// Animated mode - tween between states
self.mode = ExecutionMode::Animated;
self.tween_controller = Some(TweenController::new(queue, speed));
}
pub fn with_commands(mut self, queue: CommandQueue) -> Self {
// The TweenController will switch between instant and animated mode
// based on SetSpeed commands encountered
self.tween_controller = Some(TweenController::new(queue, self.speed));
self
}

View File

@ -1,6 +1,6 @@
//! Turtle state and world state management
use crate::general::{Angle, Color, Coordinate, Precision, Speed};
use crate::general::{Angle, AnimationSpeed, Color, Coordinate, Precision};
use crate::shapes::TurtleShape;
use macroquad::prelude::*;
@ -13,7 +13,7 @@ pub struct TurtleState {
pub color: Color,
pub fill_color: Option<Color>,
pub pen_width: Precision,
pub speed: Speed,
pub speed: AnimationSpeed,
pub visible: bool,
pub shape: TurtleShape,
}
@ -27,7 +27,7 @@ impl Default for TurtleState {
color: BLACK,
fill_color: None,
pen_width: 2.0,
speed: 100, // pixels per second
speed: AnimationSpeed::default(),
visible: true,
shape: TurtleShape::turtle(),
}
@ -35,8 +35,8 @@ impl Default for TurtleState {
}
impl TurtleState {
pub fn set_speed(&mut self, speed: Speed) {
self.speed = speed.max(1);
pub fn set_speed(&mut self, speed: AnimationSpeed) {
self.speed = speed;
}
pub fn heading_angle(&self) -> Angle {

View File

@ -2,6 +2,7 @@
use crate::circle_geometry::{CircleDirection, CircleGeometry};
use crate::commands::{CommandQueue, TurtleCommand};
use crate::general::AnimationSpeed;
use crate::state::TurtleState;
use macroquad::prelude::*;
use tween::{CubicInOut, TweenValue, Tweener};
@ -46,7 +47,7 @@ impl From<TweenVec2> for Vec2 {
pub struct TweenController {
queue: CommandQueue,
current_tween: Option<CommandTween>,
speed: f32, // pixels per second (or degrees per second for rotations)
speed: AnimationSpeed,
}
pub(crate) struct CommandTween {
@ -61,20 +62,52 @@ pub(crate) struct CommandTween {
}
impl TweenController {
pub fn new(queue: CommandQueue, speed: f32) -> Self {
pub fn new(queue: CommandQueue, speed: AnimationSpeed) -> Self {
Self {
queue,
current_tween: None,
speed: speed.max(1.0),
speed,
}
}
pub fn set_speed(&mut self, speed: f32) {
self.speed = speed.max(1.0);
pub fn set_speed(&mut self, speed: AnimationSpeed) {
self.speed = speed;
}
/// Update the tween, returns (command, start_state) if command completed
pub fn update(&mut self, state: &mut TurtleState) -> Option<(TurtleCommand, TurtleState)> {
// In immediate mode, execute all remaining commands instantly
if self.speed.is_instant() {
loop {
let command = match self.queue.next() {
Some(cmd) => cmd.clone(),
None => return None,
};
let start_state = state.clone();
// Handle SetSpeed command to potentially switch modes
if let TurtleCommand::SetSpeed(new_speed) = &command {
state.set_speed(*new_speed);
self.speed = *new_speed;
// If speed dropped below instant, switch to animated mode
if !self.speed.is_instant() {
return None;
}
continue;
}
// Execute command immediately
let target_state = self.calculate_target_state(state, &command);
*state = target_state.clone();
// Return drawable commands for rendering
if Self::command_creates_drawing(&command) {
return Some((command, start_state));
}
}
}
// Process current tween
if let Some(ref mut tween) = self.current_tween {
let elapsed = (get_time() - tween.start_time) as f32;
@ -160,6 +193,19 @@ impl TweenController {
// Start next tween
if let Some(command) = self.queue.next() {
let command_clone = command.clone();
// Handle SetSpeed command specially
if let TurtleCommand::SetSpeed(new_speed) = &command_clone {
state.set_speed(*new_speed);
self.speed = *new_speed;
// If switched to immediate mode, process immediately
if self.speed.is_instant() {
return self.update(state); // Recursively process in immediate mode
}
// For animated mode speed changes, continue to next command
return self.update(state);
}
let speed = state.speed; // Extract speed before borrowing self
let duration = self.calculate_duration(&command_clone, speed);
@ -219,8 +265,8 @@ impl TweenController {
)
}
fn calculate_duration(&self, command: &TurtleCommand, speed: u32) -> f64 {
let speed = speed.max(1) as f32;
fn calculate_duration(&self, command: &TurtleCommand, speed: AnimationSpeed) -> f64 {
let speed = speed.value();
let base_time = match command {
TurtleCommand::Move(dist) => dist.abs() / speed,