add text capabilities

This commit is contained in:
Franz Dietrich 2025-10-18 19:50:55 +02:00
parent 14b93f657b
commit 070b404bf4
9 changed files with 239 additions and 4 deletions

View File

@ -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);
}

View File

@ -1,7 +1,7 @@
//! Builder pattern traits for creating turtle command sequences //! Builder pattern traits for creating turtle command sequences
use crate::commands::{CommandQueue, TurtleCommand}; 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}; use crate::shapes::{ShapeType, TurtleShape};
/// Trait for adding commands to a queue /// Trait for adding commands to a queue
@ -635,6 +635,48 @@ impl TurtlePlan {
self 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<T>(&mut self, text: impl Into<String>, font_size: T) -> &mut Self
where
T: Into<FontSize>,
{
self.queue.push(TurtleCommand::WriteText {
text: text.into(),
font_size: font_size.into(),
});
self
}
/// Resets the turtle to its default state. /// Resets the turtle to its default state.
/// ///
/// This clears all drawings, clears the animation queue, and resets all turtle parameters: /// This clears all drawings, clears the animation queue, and resets all turtle parameters:

View File

@ -1,6 +1,6 @@
//! Turtle commands and command queue //! Turtle commands and command queue
use crate::general::{AnimationSpeed, Color, Coordinate, Precision}; use crate::general::{AnimationSpeed, Color, Coordinate, FontSize, Precision};
use crate::shapes::TurtleShape; use crate::shapes::TurtleShape;
/// Individual turtle commands /// Individual turtle commands
@ -43,6 +43,12 @@ pub enum TurtleCommand {
BeginFill, BeginFill,
EndFill, EndFill,
// Text rendering
WriteText {
text: String,
font_size: FontSize,
},
// Reset // Reset
Reset, Reset,
} }

View File

@ -31,6 +31,15 @@ pub fn render_world(world: &TurtleWorld) {
DrawCommand::Mesh { data } => { DrawCommand::Mesh { data } => {
draw_mesh(&data.to_mesh()); 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 } => { DrawCommand::Mesh { data } => {
draw_mesh(&data.to_mesh()); 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(..)) 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 /// Draw arc segments for circle tween animation
fn draw_tween_arc( fn draw_tween_arc(
tween: &crate::tweening::CommandTween, tween: &crate::tweening::CommandTween,

View File

@ -108,6 +108,17 @@ pub fn execute_command_side_effects(command: &TurtleCommand, state: &mut Turtle)
true 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::Move(_)
| TurtleCommand::Turn(_) | TurtleCommand::Turn(_)
| TurtleCommand::Circle { .. } | TurtleCommand::Circle { .. }

View File

@ -3,9 +3,11 @@
use macroquad::prelude::*; use macroquad::prelude::*;
pub mod angle; pub mod angle;
pub mod fontsize;
pub mod length; pub mod length;
pub use angle::Angle; pub use angle::Angle;
pub use fontsize::FontSize;
pub use length::Length; pub use length::Length;
/// Precision type for calculations /// Precision type for calculations

View File

@ -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<u16> for FontSize {
fn from(size: u16) -> Self {
Self(size)
}
}
impl From<f32> for FontSize {
fn from(f: f32) -> Self {
Self(f.max(1.0) as u16)
}
}
impl From<i32> for FontSize {
fn from(i: i32) -> Self {
Self(i.max(1) as u16)
}
}
impl From<i16> for FontSize {
fn from(i: i16) -> Self {
Self(i.max(1) as u16)
}
}
impl From<usize> for FontSize {
fn from(size: usize) -> Self {
Self((size as u16).max(1))
}
}

View File

@ -281,6 +281,14 @@ impl MeshData {
pub enum DrawCommand { pub enum DrawCommand {
/// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this) /// Pre-tessellated mesh data (lines, arcs, circles, polygons - all use this)
Mesh { data: MeshData }, 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 /// The complete turtle world containing all drawing state

View File

@ -432,8 +432,8 @@ impl TweenController {
TurtleCommand::SetFillColor(color) => { TurtleCommand::SetFillColor(color) => {
target.fill_color = *color; target.fill_color = *color;
} }
TurtleCommand::BeginFill | TurtleCommand::EndFill => { TurtleCommand::BeginFill | TurtleCommand::EndFill | TurtleCommand::WriteText { .. } => {
// Fill commands don't change turtle state for tweening purposes // Fill and text commands don't change turtle state for tweening purposes
// They're handled directly in execution // They're handled directly in execution
} }
TurtleCommand::Reset => { TurtleCommand::Reset => {