2025-10-17 19:16:19 +02:00

370 lines
12 KiB
Rust

//! Turtle graphics library for Macroquad
//!
//! This library provides a turtle graphics API for creating drawings and animations
//! using the Macroquad game framework.
//!
//! # Quick Start with `turtle_main` Macro
//!
//! The easiest way to create a turtle program is using the `turtle_main` macro:
//!
//! ```no_run
//! use macroquad::prelude::*;
//! use turtle_lib::*;
//!
//! #[turtle_main("My Drawing")]
//! fn draw(turtle: &mut TurtlePlan) {
//! turtle.set_pen_color(RED);
//! turtle.forward(100.0);
//! turtle.right(90.0);
//! turtle.forward(100.0);
//! }
//! ```
//!
//! The macro automatically handles window setup, rendering loop, and quit handling.
//!
//! # Manual Setup Example
//!
//! For more control, you can set up the application manually:
//!
//! ```no_run
//! use macroquad::prelude::*;
//! use turtle_lib::*;
//!
//! #[macroquad::main("Turtle")]
//! async fn main() {
//! let mut plan = create_turtle();
//! plan.forward(100.0).right(90.0).forward(100.0);
//!
//! let mut app = TurtleApp::new().with_commands(plan.build());
//!
//! loop {
//! clear_background(WHITE);
//! app.update();
//! app.render();
//! next_frame().await
//! }
//! }
//! ```
pub mod builders;
pub mod circle_geometry;
pub mod commands;
pub mod commands_channel;
pub mod drawing;
pub mod execution;
pub mod general;
pub mod shapes;
pub mod state;
pub mod tessellation;
pub mod tweening;
// Re-export commonly used types
pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands};
pub use commands::{CommandQueue, TurtleCommand};
pub use commands_channel::{turtle_command_channel, TurtleCommandReceiver, TurtleCommandSender};
pub use general::{Angle, AnimationSpeed, Color, Coordinate, Length, Precision};
pub use shapes::{ShapeType, TurtleShape};
pub use state::{DrawCommand, Turtle, TurtleWorld};
pub use tweening::TweenController;
// Re-export the turtle_main macro
pub use turtle_lib_macros::turtle_main;
// Re-export common macroquad types and colors for convenience
pub use macroquad::prelude::{
vec2, BLACK, BLUE, DARKGRAY, GOLD, GREEN, ORANGE, PURPLE, RED, WHITE, YELLOW,
};
use macroquad::prelude::*;
use std::collections::HashMap;
/// Main turtle application struct
pub struct TurtleApp {
world: TurtleWorld,
// Receivers for turtle command channels
receivers: HashMap<usize, TurtleCommandReceiver>,
// Mouse panning state
is_dragging: bool,
last_mouse_pos: Option<Vec2>,
// Zoom state
zoom_level: f32,
}
impl TurtleApp {
/// Create a new `TurtleApp` with default settings
#[must_use]
pub fn new() -> Self {
Self {
world: TurtleWorld::new(),
receivers: HashMap::new(),
is_dragging: false,
last_mouse_pos: None,
zoom_level: 1.0,
}
}
/// Add a new turtle and return its ID
pub fn add_turtle(&mut self) -> usize {
self.world.add_turtle()
}
/// Create a turtle and a command channel for it
///
/// This is the preferred way to set up turtles when using threading.
/// Call this ONCE per turtle during setup, before spawning game logic threads.
///
/// # Arguments
/// * `buffer_size` - Maximum pending command batches before sender blocks (typically 50-200)
///
/// # Returns
/// A `TurtleCommandSender` that can be cloned and sent to game logic threads.
/// The turtle is automatically managed by TurtleApp.
///
/// # Examples
/// ```no_run
/// # use turtle_lib::*;
/// # #[macroquad::main("Threading")]
/// # async fn main() {
/// let mut app = TurtleApp::new();
///
/// // Create turtle and get sender
/// let turtle_tx = app.create_turtle_channel(100);
///
/// // Send to game threads
/// let tx_clone = turtle_tx.clone();
/// std::thread::spawn(move || {
/// let mut plan = create_turtle_plan();
/// plan.forward(100.0);
/// tx_clone.send(plan.build()).ok();
/// });
/// # }
/// ```
pub fn create_turtle_channel(&mut self, buffer_size: usize) -> TurtleCommandSender {
let turtle_id = self.world.add_turtle();
let (tx, rx) = commands_channel::turtle_command_channel(turtle_id, buffer_size);
self.receivers.insert(turtle_id, rx);
tx
}
/// Process all pending commands from all turtle channels
///
/// Call this once per frame in your render loop, before `update()`.
/// Drains all receivers and applies commands to their respective turtles.
///
/// # Examples
/// ```no_run
/// # use turtle_lib::*;
/// # #[macroquad::main("Threading")]
/// # async fn main() {
/// # let mut app = TurtleApp::new();
/// # let _tx = app.create_turtle_channel(100);
/// loop {
/// clear_background(WHITE);
/// app.process_commands(); // ← Process channel commands
/// app.update();
/// app.render();
/// next_frame().await;
/// }
/// # }
/// ```
pub fn process_commands(&mut self) {
// Collect all turtle IDs to avoid borrow issues
let turtle_ids: Vec<usize> = self.receivers.keys().copied().collect();
for turtle_id in turtle_ids {
if let Some(receiver) = self.receivers.get(&turtle_id) {
for queue in receiver.recv_all() {
self.append_commands(turtle_id, queue);
}
}
}
}
/// Add commands from a turtle plan to the application for the default turtle (ID 0)
///
/// 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
#[must_use]
pub fn with_commands(self, queue: CommandQueue) -> Self {
self.with_commands_for_turtle(0, queue)
}
/// Add commands from a turtle plan to the application for a specific 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
/// * `turtle_id` - The ID of the turtle to control
/// * `queue` - The command queue to execute
#[must_use]
pub fn with_commands_for_turtle(mut self, turtle_id: usize, queue: CommandQueue) -> Self {
// Ensure turtle exists
while self.world.turtles.len() <= turtle_id {
self.world.add_turtle();
}
// Append commands to the turtle's controller
if let Some(turtle) = self.world.get_turtle_mut(turtle_id) {
turtle.tween_controller.append_commands(queue);
}
self
}
/// Execute a plan immediately on a specific turtle (no animation)
pub fn execute_immediate(&mut self, turtle_id: usize, plan: TurtlePlan) {
for ref cmd in plan.build() {
execution::execute_command_with_id(cmd, turtle_id, &mut self.world);
}
}
/// Append commands to a turtle's animation queue
pub fn append_to_queue(&mut self, turtle_id: usize, plan: TurtlePlan) {
// Ensure turtle exists
while self.world.turtles.len() <= turtle_id {
self.world.add_turtle();
}
if let Some(turtle) = self.world.get_turtle_mut(turtle_id) {
turtle.tween_controller.append_commands(plan.build());
}
}
/// Append commands from a CommandQueue to a turtle's animation queue
///
/// Used internally by `process_commands()` and can be used directly
/// when you have a `CommandQueue` instead of a `TurtlePlan`.
pub fn append_commands(&mut self, turtle_id: usize, queue: CommandQueue) {
// Ensure turtle exists
while self.world.turtles.len() <= turtle_id {
self.world.add_turtle();
}
if let Some(turtle) = self.world.get_turtle_mut(turtle_id) {
turtle.tween_controller.append_commands(queue);
}
}
/// Update animation state (call every frame)
pub fn update(&mut self) {
// Handle mouse panning and zoom
self.handle_mouse_panning();
self.handle_mouse_zoom();
// Update all turtles' tween controllers
for turtle in self.world.turtles.iter_mut() {
// Extract draw_commands and controller temporarily to avoid borrow conflicts
// Update the controller
let completed_commands = TweenController::update(turtle);
// Process all completed commands and add to the turtle's commands
for (completed_cmd, tween_start, mut end_state) in completed_commands {
let draw_command = execution::add_draw_for_completed_tween(
&completed_cmd,
&tween_start,
&mut end_state,
);
// Add the new draw commands to the turtle
turtle.commands.extend(draw_command);
}
}
}
/// Handle mouse click and drag for panning
fn handle_mouse_panning(&mut self) {
let mouse_pos = mouse_position();
let mouse_pos = vec2(mouse_pos.0, mouse_pos.1);
if is_mouse_button_pressed(MouseButton::Left) {
self.is_dragging = true;
self.last_mouse_pos = Some(mouse_pos);
}
if is_mouse_button_released(MouseButton::Left) {
self.is_dragging = false;
self.last_mouse_pos = None;
}
if self.is_dragging {
if let Some(last_pos) = self.last_mouse_pos {
// Calculate delta in screen space
let delta = mouse_pos - last_pos;
// Convert screen delta to world space delta
// The camera zoom is 2.0 / screen_width, so world_units = screen_pixels / (screen_size * zoom / 2)
let world_delta = vec2(
-delta.x, -delta.y, // Flip Y because screen Y is down
);
self.world.camera.target += world_delta * self.zoom_level;
}
self.last_mouse_pos = Some(mouse_pos);
}
}
/// Handle mouse wheel for zooming
fn handle_mouse_zoom(&mut self) {
let (_wheel_x, wheel_y) = mouse_wheel();
if wheel_y != 0.0 {
// Zoom factor: positive wheel_y = zoom in, negative = zoom out
let zoom_factor = 1.0 + wheel_y * 0.1;
self.zoom_level *= zoom_factor;
// Clamp zoom level to reasonable values
self.zoom_level = self.zoom_level.clamp(0.1, 10.0);
}
}
/// Render the turtle world (call every frame)
pub fn render(&self) {
drawing::render_world_with_tweens(&self.world, self.zoom_level);
}
/// Check if all commands have been executed
#[must_use]
pub fn is_complete(&self) -> bool {
self.world
.turtles
.iter()
.all(|turtle| turtle.tween_controller.is_complete())
}
/// Get reference to the world state
#[must_use]
pub fn world(&self) -> &TurtleWorld {
&self.world
}
/// Get mutable reference to the world state
pub fn world_mut(&mut self) -> &mut TurtleWorld {
&mut self.world
}
}
impl Default for TurtleApp {
fn default() -> Self {
Self::new()
}
}
/// Helper function to create a new turtle plan
///
/// # Example
/// ```
/// use turtle_lib::*;
///
/// let mut turtle = create_turtle();
/// turtle.forward(100.0).right(90.0).forward(50.0);
/// let commands = turtle.build();
/// ```
#[must_use]
pub fn create_turtle_plan() -> TurtlePlan {
TurtlePlan::new()
}