```rust
// Movement
plan.forward(100);
plan.backward(50);
// Rotation
plan.left(90); // degrees
plan.right(45);
// Circular arcs
plan.circle_left(50.0, 180.0, 36); // radius, angle (degrees), segments
plan.circle_right(50.0, 180.0, 36); // draws arc to the right
// Pen control
plan.pen_up();
plan.pen_down();
// Appearance
plan.set_color(RED);
plan.set_pen_width(5.0);
plan.hide();
plan.show();
// Turtle shape
plan.shape(ShapeType::Triangle);
plan.shape(ShapeType::Turtle); // Default classic turtle shape
plan.shape(ShapeType::Circle);
plan.shape(ShapeType::Square);
plan.shape(ShapeType::Arrow);
// Custom shape
let custom = TurtleShape::new(
vec![vec2(10.0, 0.0), vec2(-5.0, 5.0), vec2(-5.0, -5.0)],
true // filled
);
plan.set_shape(custom);
// Chaining
plan.forward(100).right(90).forward(50);
```
208 lines
7.7 KiB
Rust
208 lines
7.7 KiB
Rust
//! Circle geometry calculations - single source of truth for circle_left and circle_right
|
|
|
|
use macroquad::prelude::*;
|
|
|
|
/// Direction of circular motion (in screen coordinates with Y-down)
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum CircleDirection {
|
|
Left, // Counter-clockwise visually, heading decreases
|
|
Right, // Clockwise visually, heading increases
|
|
}
|
|
|
|
/// Encapsulates all geometry for a circular arc
|
|
pub struct CircleGeometry {
|
|
pub center: Vec2,
|
|
pub radius: f32,
|
|
pub start_angle_from_center: f32, // radians
|
|
pub direction: CircleDirection,
|
|
}
|
|
|
|
impl CircleGeometry {
|
|
/// Create geometry for a circle command
|
|
pub fn new(
|
|
turtle_pos: Vec2,
|
|
turtle_heading: f32,
|
|
radius: f32,
|
|
direction: CircleDirection,
|
|
) -> Self {
|
|
use std::f32::consts::FRAC_PI_2;
|
|
|
|
// Calculate center based on direction
|
|
// In screen coordinates (Y-down):
|
|
// - Left turn (counter-clockwise visually): center is perpendicular-left from turtle's perspective
|
|
// which is heading - π/2 (rotated clockwise from heading vector)
|
|
// - Right turn (clockwise visually): center is perpendicular-right from turtle's perspective
|
|
// which is heading + π/2 (rotated counter-clockwise from heading vector)
|
|
let center_offset_angle = match direction {
|
|
CircleDirection::Left => turtle_heading - FRAC_PI_2,
|
|
CircleDirection::Right => turtle_heading + FRAC_PI_2,
|
|
};
|
|
|
|
let center = vec2(
|
|
turtle_pos.x + radius * center_offset_angle.cos(),
|
|
turtle_pos.y + radius * center_offset_angle.sin(),
|
|
);
|
|
|
|
// Angle from center back to turtle position
|
|
let start_angle_from_center = match direction {
|
|
CircleDirection::Left => turtle_heading + FRAC_PI_2,
|
|
CircleDirection::Right => turtle_heading - FRAC_PI_2,
|
|
};
|
|
|
|
Self {
|
|
center,
|
|
radius,
|
|
start_angle_from_center,
|
|
direction,
|
|
}
|
|
}
|
|
|
|
/// Calculate position after traveling an angle along the arc
|
|
pub fn position_at_angle(&self, angle_traveled: f32) -> Vec2 {
|
|
let current_angle = match self.direction {
|
|
CircleDirection::Left => self.start_angle_from_center - angle_traveled,
|
|
CircleDirection::Right => self.start_angle_from_center + angle_traveled,
|
|
};
|
|
|
|
vec2(
|
|
self.center.x + self.radius * current_angle.cos(),
|
|
self.center.y + self.radius * current_angle.sin(),
|
|
)
|
|
}
|
|
|
|
/// Calculate position at a given progress (0.0 to 1.0) through total_angle
|
|
pub fn position_at_progress(&self, total_angle: f32, progress: f32) -> Vec2 {
|
|
let angle_traveled = total_angle * progress;
|
|
self.position_at_angle(angle_traveled)
|
|
}
|
|
|
|
/// Get the angle traveled from start position to a given position
|
|
pub fn angle_to_position(&self, position: Vec2) -> f32 {
|
|
let displacement = position - self.center;
|
|
let current_angle = displacement.y.atan2(displacement.x);
|
|
|
|
let mut angle_diff = match self.direction {
|
|
CircleDirection::Left => self.start_angle_from_center - current_angle,
|
|
CircleDirection::Right => current_angle - self.start_angle_from_center,
|
|
};
|
|
|
|
// Normalize to [0, 2π)
|
|
if angle_diff < 0.0 {
|
|
angle_diff += 2.0 * std::f32::consts::PI;
|
|
}
|
|
|
|
angle_diff
|
|
}
|
|
|
|
/// Get draw_arc parameters for the full arc
|
|
/// Returns (rotation_degrees, arc_degrees) for macroquad's draw_arc
|
|
pub fn draw_arc_params(&self, total_angle_degrees: f32) -> (f32, f32) {
|
|
match self.direction {
|
|
CircleDirection::Left => {
|
|
// For left (counter-clockwise), we need to draw counter-clockwise from end back to start
|
|
// so we start at (start - total_angle) and draw total_angle counter-clockwise
|
|
let end_angle = self.start_angle_from_center - total_angle_degrees.to_radians();
|
|
(end_angle.to_degrees(), total_angle_degrees)
|
|
}
|
|
CircleDirection::Right => {
|
|
// For right (clockwise), draw from start
|
|
(
|
|
self.start_angle_from_center.to_degrees(),
|
|
total_angle_degrees,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Get draw_arc parameters for a partial arc (during tweening)
|
|
/// Returns (rotation_degrees, arc_degrees) for macroquad's draw_arc
|
|
pub fn draw_arc_params_partial(&self, angle_traveled: f32) -> (f32, f32) {
|
|
let angle_traveled_degrees = angle_traveled.to_degrees();
|
|
|
|
match self.direction {
|
|
CircleDirection::Left => {
|
|
// Draw from current position backwards (counter-clockwise) to start
|
|
let current_angle = self.start_angle_from_center - angle_traveled;
|
|
(current_angle.to_degrees(), angle_traveled_degrees)
|
|
}
|
|
CircleDirection::Right => {
|
|
// Draw from start, counter-clockwise
|
|
(
|
|
self.start_angle_from_center.to_degrees(),
|
|
angle_traveled_degrees,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::f32::consts::{FRAC_PI_2, PI};
|
|
|
|
#[test]
|
|
fn test_circle_left_geometry() {
|
|
let geom = CircleGeometry::new(
|
|
vec2(0.0, 0.0),
|
|
0.0, // heading east (0 radians)
|
|
100.0,
|
|
CircleDirection::Left,
|
|
);
|
|
|
|
// For left turn with heading east (0), center should be at heading - π/2
|
|
// That's -π/2 radians = south
|
|
// Center = start + 100 * (cos(-π/2), sin(-π/2)) = (0, 0) + (0, -100) = (0, -100)
|
|
assert!(
|
|
(geom.center.x - 0.0).abs() < 0.01,
|
|
"center.x = {}",
|
|
geom.center.x
|
|
);
|
|
assert!(
|
|
(geom.center.y - (-100.0)).abs() < 0.01,
|
|
"center.y = {}",
|
|
geom.center.y
|
|
);
|
|
|
|
// After π/2 radians counter-clockwise around a circle centered at (0, -100):
|
|
// start_angle = π/2 (pointing north from center, which is where (0,0) is)
|
|
// after π/2 counter-clockwise (subtract in screen coords): angle = π/2 - π/2 = 0 (pointing east from center)
|
|
// pos = (0, -100) + 100 * (cos(0), sin(0)) = (0, -100) + (100, 0) = (100, -100)
|
|
let pos = geom.position_at_angle(FRAC_PI_2);
|
|
assert!((pos.x - 100.0).abs() < 0.01, "pos.x = {}", pos.x);
|
|
assert!((pos.y - (-100.0)).abs() < 0.01, "pos.y = {}", pos.y);
|
|
}
|
|
|
|
#[test]
|
|
fn test_circle_right_geometry() {
|
|
let geom = CircleGeometry::new(
|
|
vec2(0.0, 0.0),
|
|
0.0, // heading east
|
|
100.0,
|
|
CircleDirection::Right,
|
|
);
|
|
|
|
// For right turn with heading east (0), center should be at heading + π/2
|
|
// That's π/2 radians = north
|
|
// Center = start + 100 * (cos(π/2), sin(π/2)) = (0, 0) + (0, 100) = (0, 100)
|
|
assert!(
|
|
(geom.center.x - 0.0).abs() < 0.01,
|
|
"center.x = {}",
|
|
geom.center.x
|
|
);
|
|
assert!(
|
|
(geom.center.y - 100.0).abs() < 0.01,
|
|
"center.y = {}",
|
|
geom.center.y
|
|
);
|
|
|
|
// After π/2 radians clockwise around a circle centered at (0, 100):
|
|
// start_angle = -π/2 (pointing south from center, which is where (0,0) is)
|
|
// after π/2 clockwise (add in screen coords): angle = -π/2 + π/2 = 0 (pointing east from center)
|
|
// pos = (0, 100) + 100 * (cos(0), sin(0)) = (0, 100) + (100, 0) = (100, 100)
|
|
let pos = geom.position_at_angle(PI / 2.0);
|
|
assert!((pos.x - 100.0).abs() < 0.01, "pos.x = {}", pos.x);
|
|
assert!((pos.y - 100.0).abs() < 0.01, "pos.y = {}", pos.y);
|
|
}
|
|
}
|