2D turtle graphics


Turtle Spiral Generator is a thing based on turtle graphics.

Imagine that a turtle is crawling and leaving footprints on a beach. It's a real turtle graphics. When leveraging turtle graphics, we just give commands such as forward, turn and so on. The turtle hide all the details about coordinates.

The concept behind Turtle graphics is not hard. Different programming languages have different implementations of turtle graphics because of their different paradigm. OpenSCAD has a Functional programming paradigm. Its variables and vectors are immutable. If you are not familiar with Functional programming, you might encounter some difficulties when realizing turtle graphics.

Positions and angles

When commanding a turtle to forward or turn, we have to trace the current coordinate and angle at a low level. I use the vector [[x, y], angle] to represent these data. For convenience, I define a turtle function which returns [[x, y], angle] if you pass the values of parameters x, y and angle.

function turtle(x, y, angle) = [[x, y], angle];

function get_x(turtle) = turtle[0][0];  // return x
function get_y(turtle) = turtle[0][1];  // return y
function get_xy(turtle) = turtle[0];    // return [x, y]
function get_angle(turtle) = turtle[1]; // return angle

Once you have a turtle data, you might want to change its coordinate but leave its angle unchanged. Because a vector is immutable in OpenSCAD, you cannot write code as below.

t = turtle(0, 0, 0);
// change to [10, 10] 
t[0][0] = 10;
t[0][1] = 10;

What can we do? You can create a new vector including the new coordinate and original angle.

function set_point(turtle, point) = [point, get_angle(turtle)];

Then, you can get a new turtle like this.

t = turtle(0, 0, 0);
// get a new turtle
new_t = set_point(t, [10, 10]);

The original data refered by t is unchanged. You have to command the new turtle referred by new_t. For convenience, you may also define set_x, set_y and set_angle functions.

function set_x(turtle, x) = [[x, get_y(turtle)], get_angle(turtle)]; // set x
function set_y(turtle, y) = [[get_x(turtle), y], get_angle(turtle)]; // set y
function set_angle(turtle, angle) = [get_xy(turtle), angle];         // set angle

Forwarding a turtle

If you forward a turtle, it will draw a line on the path. In imperative languages, you can easily implement such a forward(leng) module which draws a line after forwarding. But, you'll soon realize that you cannot do it in OpenSCAD.

In OpenSCAD, drawing is an action with side effects. You should use module to define these actions, but a module cannot return a value. If you want to return a value, you should use function.

So you cannot define a forward(leng) module which forwards a turtle, draws a line and then return a new turtle data. How to solve this problem? Think about it. In imperative languages, how do you draw a line after forwarding a turtle? You will reserve the original turtle data, forward the turtle and use both turtle data to draw a line.

We can use the polyline module developed in Line to draw a line, so we only have to retrieve a new turtle after forwarding.

function forward(turtle, leng) = 
    turtle(
        get_x(turtle) + leng * cos(get_angle(turtle)), 
        get_y(turtle) + leng * sin(get_angle(turtle)), 
        get_angle(turtle)
    );

Now, drawing a line after forwarding a turtle requires two steps.

leng = 10;
width = 1;

t = turtle(0, 0, 0);

new_t = forword(t, leng);
polyline([get_xy(t), get_xy(new_t)], width);

If you want to move the turtle to a new coordinate and draw a line, how to do it? Because the set_point function returns a new turtle, the polyline module can easily do this job.

width = 1;

t = turtle(0, 0, 0);

// move to [10, 10]
new_t = set_point(t, [10, 10]);
polyline([get_xy(t), get_xy(new_t)], width);

Turning a turtle

From the above, it should be easy for you to define a turn function.

function turn(turtle, angle) = [get_xy(turtle), get_angle(turtle) + angle];

Then, how about using our turtle to draw a triangle?

function turtle(x, y, angle) = [[x, y], angle];

function get_x(turtle) = turtle[0][0];
function get_y(turtle) = turtle[0][1];
function get_xy(turtle) = turtle[0];
function get_angle(turtle) = turtle[1];

function set_point(turtle, point) = [point, get_angle(turtle)];

function set_x(turtle, x) = [[x, get_y(turtle)], get_angle(turtle)];
function set_y(turtle, y) = [[get_x(turtle), y], get_angle(turtle)];
function set_angle(turtle, angle) = [get_xy(turtle), angle];

function forward(turtle, leng) = 
    turtle(
        get_x(turtle) + leng * cos(get_angle(turtle)), 
        get_y(turtle) + leng * sin(get_angle(turtle)), 
        get_angle(turtle)
    );

function turn(turtle, angle) = [get_xy(turtle), get_angle(turtle) + angle];

module line(point1, point2, width = 1, cap_round = true) {
    angle = 90 - atan((point2[1] - point1[1]) / (point2[0] - point1[0]));
    offset_x = 0.5 * width * cos(angle);
    offset_y = 0.5 * width * sin(angle);

    offset1 = [-offset_x, offset_y];
    offset2 = [offset_x, -offset_y];

    if(cap_round) {
        translate(point1) circle(d = width, $fn = 24);
        translate(point2) circle(d = width, $fn = 24);
    }

    polygon(points=[
        point1 + offset1, point2 + offset1,  
        point2 + offset2, point1 + offset2
    ]);
}

module polyline(points, width = 1) {
    module polyline_inner(points, index) {
        if(index < len(points)) {
            line(points[index - 1], points[index], width);
            polyline_inner(points, index + 1);
        }
    }

    polyline_inner(points, 1);
}

side_leng = 10;
angle = 120; 
width = 1;

t = turtle(0, 0, 0);

t_p1 = forward(t, side_leng);                   // forward side_leng
polyline([get_xy(t), get_xy(t_p1)], width);     // draw a line

t_p2 = forward(turn(t_p1, angle), side_leng);   // turn angle and forward side_leng
polyline([get_xy(t_p1), get_xy(t_p2)], width);  // draw a line

t_p3 = forward(turn(t_p2, angle), side_leng);   // turn angle and forward side_leng
polyline([get_xy(t_p2), get_xy(t_p3)], width);  // draw a line

The triangle drawn by the turtle is as below.

2D turtle graphics

It's different from the imperative paradigm, right? You might be not used to Functional programming in the beginning. Once you are used to the paradigm, however, you'll draw much inspiration from it.