We refactored the builder pattern in [`turtle-lib`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib) to eliminate inherent method asymmetry and organize all turtle capabilities into six cohesive traits. [`builders.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs) The legacy traits (`DirectionalMovement`, `Turnable`, `CurvedMovement`) and orphaned inherent methods have been reorganized into six domain-focused traits: - **[`Movement`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L14)**: - `forward()` - `backward()` - `go_to()` - `circle_left()` - `circle_right()` - **[`Rotation`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L191)**: - `left()` - `right()` - `set_heading()` - **[`Pen`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L279)**: - `pen_up()` - `pen_down()` - `set_pen_color()` - `set_pen_width()` - **[`Fill`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L393)**: - `begin_fill()` - `end_fill()` - `set_fill_color()` - **[`Cursor`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L481)**: - `hide()` - `show()` - `shape()` - `set_shape()` - `set_speed()` - `reset()` - **[`Text`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L646)**: - `write_text()` [`TurtlePlan`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/builders.rs#L688) `TurtlePlan`'s inherent methods are now strictly builder lifecycle controls: - `new() -> Self` - `build(self) -> CommandQueue` `TurtlePlan` implements `WithCommands`, `Movement`, `Rotation`, `Pen`, `Fill`, `Cursor`, and `Text`. - **[`lib.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/lib.rs#L62-L65)**: Re-exports `Cursor`, `Fill`, `Movement`, `Pen`, `Rotation`, `Text`, `TurtlePlan`, `WithCommands`. - **Examples**: Updated [`clock.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/clock.rs#L8), [`clock_threaded.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/clock_threaded.rs#L9), [`dashed_circle.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/dashed_circle.rs#L4), and [`bezier.rs`](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/bezier.rs#L4) to use `use turtle_lib::*;`. - **[`README.md`](file:///home/dietrich/Projekte/Source/turtlers/README.md#L344)**: Updated trait references in the architecture outline. --- - **Unit & Doc Tests**: ```bash cargo test --package turtle-lib ``` Result: 17 unit tests passed; 34 doc-tests passed (0 failed). - **All Examples**: ```bash cargo check --package turtle-lib --examples ``` Result: Successfully compiled all 30 examples. - **Clippy**: ```bash cargo clippy --package turtle-lib -- -Wclippy::pedantic \ -Aclippy::cast_precision_loss -Aclippy::cast_sign_loss -Aclippy::cast_possible_truncation ``` Result: 0 warnings in `builders.rs`.
425 lines
14 KiB
Rust
425 lines
14 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();
|
|
//! 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(crate) mod builders;
|
|
pub(crate) mod circle_geometry;
|
|
pub(crate) mod command_behavior;
|
|
pub(crate) mod commands;
|
|
pub(crate) mod commands_channel;
|
|
pub(crate) mod drawing;
|
|
pub(crate) mod execution;
|
|
pub(crate) mod general;
|
|
pub(crate) mod shapes;
|
|
pub(crate) mod state;
|
|
pub(crate) mod tessellation;
|
|
pub(crate) mod tweening;
|
|
|
|
pub use builders::{
|
|
Cursor, Fill, Movement, Pen, Rotation, Text, TurtlePlan, WithCommands,
|
|
};
|
|
pub use commands::{CommandQueue, TurtleCommand};
|
|
pub use commands_channel::TurtleCommandSender;
|
|
pub use general::{AnimationSpeed, Color, Coordinate, Degrees, Length, Precision, Radians};
|
|
pub use shapes::{ShapeType, TurtleShape};
|
|
|
|
pub mod export;
|
|
#[cfg(feature = "svg")]
|
|
pub(crate) mod export_svg;
|
|
|
|
// Re-export the turtle_main macro
|
|
pub use turtle_lib_macros::turtle_main;
|
|
|
|
// Re-export the macroquad crate so generated macro code can access it directly
|
|
pub use macroquad;
|
|
|
|
// 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 crate::commands_channel::TurtleCommandReceiver;
|
|
use crate::state::TurtleWorld;
|
|
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 {
|
|
/// Export the current drawing to a file in the specified format.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if the export fails (e.g., unsupported format, file I/O error)
|
|
pub fn export_drawing(
|
|
&self,
|
|
filename: &str,
|
|
format: export::DrawingFormat,
|
|
) -> Result<(), export::ExportError> {
|
|
let _ = filename;
|
|
match format {
|
|
#[cfg(feature = "svg")]
|
|
export::DrawingFormat::Svg => {
|
|
use crate::export::DrawingExporter;
|
|
use export_svg::svg_export::SvgExporter;
|
|
let exporter = SvgExporter;
|
|
exporter.export(&self.world, filename)
|
|
}
|
|
// Additional formats can be registered here.
|
|
#[allow(unreachable_patterns)]
|
|
_ => Err(export::ExportError::Format(
|
|
"Unsupported export format".to_string(),
|
|
)),
|
|
}
|
|
}
|
|
/// 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::*;
|
|
/// # use macroquad::prelude::{next_frame, clear_background, WHITE};
|
|
/// # #[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 >= 1000 = instant mode, speed < 1000 = 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 >= 1000 = instant mode, speed < 1000 = 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) {
|
|
// Ensure turtle exists
|
|
while self.world.turtles.len() <= turtle_id {
|
|
self.world.add_turtle();
|
|
}
|
|
|
|
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 and process window mouse events (call every frame in GUI loop)
|
|
pub fn update(&mut self) {
|
|
// Handle mouse panning and zoom
|
|
self.handle_mouse_panning();
|
|
self.handle_mouse_zoom();
|
|
|
|
self.step_animations();
|
|
}
|
|
|
|
/// Drive animation updates for all turtles without querying window or mouse events.
|
|
///
|
|
/// Suitable for headless execution (such as CLI SVG export) where no graphics window exists.
|
|
pub fn step_animations(&mut self) {
|
|
// Update all turtles' tween controllers
|
|
for turtle in &mut self.world.turtles {
|
|
// Drive this turtle's animation controller for one frame.
|
|
// `update_tweens` splits &mut Turtle into disjoint field borrows so
|
|
// TweenController::update can be a proper &mut self method.
|
|
let completed_commands = turtle.update_tweens();
|
|
|
|
// Process all completed commands and add to the turtle's commands
|
|
for (completed_cmd, tween_start, end_state) in completed_commands {
|
|
if let Some(draw_cmd) =
|
|
execution::tessellate_command(&completed_cmd, &tween_start, end_state.position)
|
|
{
|
|
#[cfg(feature = "svg")]
|
|
execution::push_svg_for_draw(
|
|
&completed_cmd,
|
|
&tween_start,
|
|
end_state.position,
|
|
&mut turtle.svg_log,
|
|
);
|
|
turtle.commands.push(draw_cmd);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/// 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())
|
|
}
|
|
|
|
/// Set the animation speed for all turtles
|
|
///
|
|
/// # Arguments
|
|
/// * `speed` - The animation speed to set for all turtles
|
|
pub fn set_all_turtles_speed(&mut self, speed: AnimationSpeed) {
|
|
for turtle in &mut self.world.turtles {
|
|
turtle.set_speed(speed);
|
|
turtle.tween_controller.set_speed(speed);
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
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_plan();
|
|
/// turtle.forward(100.0).right(90.0).forward(50.0);
|
|
/// let commands = turtle.build();
|
|
/// ```
|
|
#[must_use]
|
|
pub fn create_turtle_plan() -> TurtlePlan {
|
|
TurtlePlan::new()
|
|
}
|