From 070b404bf4f16c8f5db35c159fe59587bd8fb8a9 Mon Sep 17 00:00:00 2001 From: Franz Dietrich Date: Sat, 18 Oct 2025 19:50:55 +0200 Subject: [PATCH] add text capabilities --- turtle-lib/examples/text_demo.rs | 64 ++++++++++++++++++++++++++++++ turtle-lib/src/builders.rs | 44 +++++++++++++++++++- turtle-lib/src/commands.rs | 8 +++- turtle-lib/src/drawing.rs | 54 +++++++++++++++++++++++++ turtle-lib/src/execution.rs | 11 +++++ turtle-lib/src/general.rs | 2 + turtle-lib/src/general/fontsize.rs | 48 ++++++++++++++++++++++ turtle-lib/src/state.rs | 8 ++++ turtle-lib/src/tweening.rs | 4 +- 9 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 turtle-lib/examples/text_demo.rs create mode 100644 turtle-lib/src/general/fontsize.rs diff --git a/turtle-lib/examples/text_demo.rs b/turtle-lib/examples/text_demo.rs new file mode 100644 index 0000000..4fbbcde --- /dev/null +++ b/turtle-lib/examples/text_demo.rs @@ -0,0 +1,64 @@ +//! Demonstration of text rendering with turtle heading orientation + +use macroquad::prelude::*; +use turtle_lib::*; + +#[turtle_main("Text Demo")] +fn draw(turtle: &mut TurtlePlan) { + // Write text at heading 0 (right) + turtle + .set_pen_color(BLACK) + .write_text("Heading 0°", 20u16) + .forward(0.0); // Just to complete the chain + + // Move forward and turn, then write text at different angles + turtle + .forward(100.0) + .right(45.0) + .write_text("45° right", 18u16) + .forward(0.0); + + turtle + .forward(80.0) + .right(45.0) + .write_text("90° down", 18u16) + .forward(0.0); + + turtle + .forward(80.0) + .right(45.0) + .write_text("135°", 18u16) + .forward(0.0); + + turtle + .forward(80.0) + .right(45.0) + .write_text("180° left", 18u16) + .forward(0.0); + + // Use different font sizes + turtle + .pen_up() + .go_to(vec2(-200.0, 100.0)) + .pen_down() + .set_pen_color(BLUE) + .write_text("Small", 12f32) + .forward(50.0) + .write_text("Medium", 20) + .forward(50.0) + .write_text("Large", 28u16) + .forward(0.0); + + // Example with drawing + turtle + .pen_up() + .go_to(vec2(0.0, -150.0)) + .pen_down() + .set_pen_color(RED) + .circle_right(50.0, 360.0, 32) + .pen_up() + .go_to(vec2(0.0, -150.0)) + .pen_down() + .write_text("Circle", 16f32) + .forward(0.0); +} diff --git a/turtle-lib/src/builders.rs b/turtle-lib/src/builders.rs index 50b3ac0..238926d 100644 --- a/turtle-lib/src/builders.rs +++ b/turtle-lib/src/builders.rs @@ -1,7 +1,7 @@ //! Builder pattern traits for creating turtle command sequences use crate::commands::{CommandQueue, TurtleCommand}; -use crate::general::{AnimationSpeed, Color, Coordinate, Precision}; +use crate::general::{AnimationSpeed, Color, Coordinate, FontSize, Precision}; use crate::shapes::{ShapeType, TurtleShape}; /// Trait for adding commands to a queue @@ -635,6 +635,48 @@ impl TurtlePlan { self } + /// Writes text at the turtle's current position, oriented along its heading direction. + /// + /// The text is rendered with its baseline positioned slightly above the turtle's current position, + /// and rotated to align with the turtle's current heading. + /// + /// # Arguments + /// + /// * `text` - The text to render (can be `&str` or `String`) + /// * `font_size` - The font size, can be any type that converts to `FontSize` (e.g., `f32`, `u16`, `i32`) + /// + /// # Examples + /// + /// ```no_run + /// # use turtle_lib::*; + /// # + /// #[turtle_main("Text Example")] + /// fn draw(turtle: &mut TurtlePlan) { + /// // Write text at current position (heading 0° = horizontal) + /// turtle.write_text("Hello", 20.0); + /// + /// // Move forward and write at an angle + /// turtle.forward(100.0) + /// .right(45.0) + /// .write_text("World", 24); + /// + /// // Chain with other commands + /// turtle.forward(50.0) + /// .write_text("End", 16u16); + /// } + /// ``` + #[must_use] + pub fn write_text(&mut self, text: impl Into, font_size: T) -> &mut Self + where + T: Into, + { + self.queue.push(TurtleCommand::WriteText { + text: text.into(), + font_size: font_size.into(), + }); + self + } + /// Resets the turtle to its default state. /// /// This clears all drawings, clears the animation queue, and resets all turtle parameters: diff --git a/turtle-lib/src/commands.rs b/turtle-lib/src/commands.rs index bedf417..b6b3e8d 100644 --- a/turtle-lib/src/commands.rs +++ b/turtle-lib/src/commands.rs @@ -1,6 +1,6 @@ //! Turtle commands and command queue -use crate::general::{AnimationSpeed, Color, Coordinate, Precision}; +use crate::general::{AnimationSpeed, Color, Coordinate, FontSize, Precision}; use crate::shapes::TurtleShape; /// Individual turtle commands @@ -43,6 +43,12 @@ pub enum TurtleCommand { BeginFill, EndFill, + // Text rendering + WriteText { + text: String, + font_size: FontSize, + }, + // Reset Reset, } diff --git a/turtle-lib/src/drawing.rs b/turtle-lib/src/drawing.rs index 2763a28..a1a9237 100644 --- a/turtle-lib/src/drawing.rs +++ b/turtle-lib/src/drawing.rs @@ -31,6 +31,15 @@ pub fn render_world(world: &TurtleWorld) { DrawCommand::Mesh { data } => { draw_mesh(&data.to_mesh()); } + DrawCommand::Text { + text, + position, + heading, + font_size, + color, + } => { + draw_text_command(text, *position, *heading, *font_size, *color); + } } } } @@ -70,6 +79,15 @@ pub fn render_world_with_tweens(world: &TurtleWorld, zoom_level: f32) { DrawCommand::Mesh { data } => { draw_mesh(&data.to_mesh()); } + DrawCommand::Text { + text, + position, + heading, + font_size, + color, + } => { + draw_text_command(text, *position, *heading, *font_size, *color); + } } } } @@ -284,6 +302,42 @@ fn should_draw_tween_line(command: &crate::commands::TurtleCommand) -> bool { matches!(command, TurtleCommand::Move(..) | TurtleCommand::Goto(..)) } +/// Draw a text command with rotation based on turtle heading +fn draw_text_command( + text: &str, + position: Vec2, + heading_radians: f32, + font_size: crate::general::FontSize, + color: Color, +) { + // Heading in turtle coordinates: 0 rad = right, positive = counter-clockwise + // Macroquad rotation: same convention (0 = right, positive = counter-clockwise) + // So we use the heading directly + let rotation_rad = heading_radians; + + // Calculate perpendicular offset (90° clockwise from heading) + // This places text slightly to the right of the movement direction + let font_size_val = font_size.value(); + let offset_distance = f32::from(font_size_val) / 3.0; + + // Perpendicular direction: heading - π/2 (rotated 90° clockwise) + let perpendicular_angle = heading_radians - std::f32::consts::PI / 2.0; + let offset_x = offset_distance * perpendicular_angle.cos(); + let offset_y = offset_distance * perpendicular_angle.sin(); + + draw_text_ex( + text, + position.x + offset_x, + position.y + offset_y, + TextParams { + font_size: font_size_val, + rotation: rotation_rad, + color, + ..Default::default() + }, + ); +} + /// Draw arc segments for circle tween animation fn draw_tween_arc( tween: &crate::tweening::CommandTween, diff --git a/turtle-lib/src/execution.rs b/turtle-lib/src/execution.rs index a54f3b5..3b068c8 100644 --- a/turtle-lib/src/execution.rs +++ b/turtle-lib/src/execution.rs @@ -108,6 +108,17 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle) true } + TurtleCommand::WriteText { text, font_size } => { + state.commands.push(DrawCommand::Text { + text: text.clone(), + position: state.params.position, + heading: state.params.heading, + font_size: *font_size, + color: state.params.color, + }); + true + } + TurtleCommand::Move(_) | TurtleCommand::Turn(_) | TurtleCommand::Circle { .. } diff --git a/turtle-lib/src/general.rs b/turtle-lib/src/general.rs index e1b2fc5..c726bc0 100644 --- a/turtle-lib/src/general.rs +++ b/turtle-lib/src/general.rs @@ -3,9 +3,11 @@ use macroquad::prelude::*; pub mod angle; +pub mod fontsize; pub mod length; pub use angle::Angle; +pub use fontsize::FontSize; pub use length::Length; /// Precision type for calculations diff --git a/turtle-lib/src/general/fontsize.rs b/turtle-lib/src/general/fontsize.rs new file mode 100644 index 0000000..e702fec --- /dev/null +++ b/turtle-lib/src/general/fontsize.rs @@ -0,0 +1,48 @@ +//! `FontSize` type for text rendering + +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct FontSize(pub u16); + +impl FontSize { + /// Create a new `FontSize` from a u16 value + #[must_use] + pub const fn new(size: u16) -> Self { + Self(size) + } + + /// Get the inner u16 value + #[must_use] + pub const fn value(&self) -> u16 { + self.0 + } +} + +impl From for FontSize { + fn from(size: u16) -> Self { + Self(size) + } +} + +impl From for FontSize { + fn from(f: f32) -> Self { + Self(f.max(1.0) as u16) + } +} + +impl From for FontSize { + fn from(i: i32) -> Self { + Self(i.max(1) as u16) + } +} + +impl From for FontSize { + fn from(i: i16) -> Self { + Self(i.max(1) as u16) + } +} + +impl From for FontSize { + fn from(size: usize) -> Self { + Self((size as u16).max(1)) + } +} diff --git a/turtle-lib/src/state.rs b/turtle-lib/src/state.rs index 58c740a..7590894 100644 --- a/turtle-lib/src/state.rs +++ b/turtle-lib/src/state.rs @@ -281,6 +281,14 @@ impl MeshData { pub enum DrawCommand { /// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this) Mesh { data: MeshData }, + /// Text rendering command + Text { + text: String, + position: Vec2, + heading: f32, + font_size: crate::general::FontSize, + color: Color, + }, } /// The complete turtle world containing all drawing state diff --git a/turtle-lib/src/tweening.rs b/turtle-lib/src/tweening.rs index 60ec351..4896735 100644 --- a/turtle-lib/src/tweening.rs +++ b/turtle-lib/src/tweening.rs @@ -432,8 +432,8 @@ impl TweenController { TurtleCommand::SetFillColor(color) => { target.fill_color = *color; } - TurtleCommand::BeginFill | TurtleCommand::EndFill => { - // Fill commands don't change turtle state for tweening purposes + TurtleCommand::BeginFill | TurtleCommand::EndFill | TurtleCommand::WriteText { .. } => { + // Fill and text commands don't change turtle state for tweening purposes // They're handled directly in execution } TurtleCommand::Reset => {