Line


Line? What a weird topic? Many models need lines, such as Turtle Spiral Generator and Symmetrical triangle generator. They are all based on lines.

Unfortunately, OpenSCAD lacks a built-in module named polyline. What should we do now?

The polygon module

OpenSCAD has a built-in polygon module which accepts a list of [x, y] coordinates to create a multi-sided shape. The official documentation provides several simple examples. The polygon module also has a paths parameter which indicates the order to traverse the points.

polygon(points=[[0,0],[100,0],[130,50],[30,50]]);
 polygon([[0,0],[100,0],[130,50],[30,50]], paths=[[0,1,2,3]]);
 polygon([[0,0],[100,0],[130,50],[30,50]],[[3,2,1,0]]);
 polygon([[0,0],[100,0],[130,50],[30,50]],[[1,0,3,2]]);

The code fragments create the same parallelogram.

polygon

A line from two points

Why do you have to know the polygon module? After all, it's for creating a multi-sided shape, not for creating a line, right? Well, let's back to the topic. If you have to define a polyline module which accepts a list of points and connects all of them automatically, what do you have to solve first?

Divide and conquer is an essential skill of programming. The minimum task of the above requirement is connecting two points to form a line.

Maybe the first thought which comes to your mind is that OpenSCAD has a built-in square module. If it has two relatively short sides, the square looks like a line. Next, calculate the length between two points, the angle between the line and x-axis, rotate the line and move it to the right position and so on. Wow, it's troublesome.

Here is the second thought. OpenSCAD has a built-in polygon module. It accepts a list of [x, y] coordinates to create a multiple sided shape. Can we give it four points, which the last two points are calculated from the previous two points and the line width, to form a line? According to the thought, we implement a prototype below.

module line(p1, p2, width) {
    polygon([
        p1, p2, [p2[0], p2[1] - width], [p1[0], p1[1] - width]
    ]);
}

line([1, 2], [-5, -4], 1);

It seems pretty good.

Line

Here, the last two points directly come from moving the previous two points vertically. If you are picky, you might say this is wrong. I cannot just move the two points vertically. It should be the distance between two parallel lines.

Well, I am also picky so let's be picky thoroughly. I want a line like this.

Line

That is, the line is aligned center. According to the figure, we can derive a formula.

angle = atan((p2[1] - p1[1]) / (p2[0] - p1[0]));

Then, the four points passed into polygon is …

offset_x = 0.5 * width * cos(90 - angle);
offset_y = 0.5 * width * sin(90 - angle);
offset1 = [-offset_x, offset_y];
offset2 = [offset_x, -offset_y];
points=[
    point1 + offset1, point2 + offset1,  
    point2 + offset2, point1 + offset2
];

Let's refactor the above code and define a module for it.

module line(point1, point2, width = 1) {
    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];

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

line([1, 2], [-5, -4], 1);

We have a line now.

Line

polyline

Once a line module which can draw a line from two points is ready, we can implement a polyline module based on it. You just take two points and draw a line sequentially until consuming all points.

module line(point1, point2, width = 1) {
    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];

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

polyline([[1, 2], [-5, -4], [-5, 3], [5, 5]], 1);

The code creates a model like this.

Line

Hmm? What are those gaps? The reason is the line module generates a square. One simple solution is adding a small circle at the point.

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

polyline([[1, 2], [-5, -4], [-5, 3], [5, 5]], 1);

Finally, we get a polyline.

Line

How about this polyline? It's just a simple implementation. There are still other implementations. For example, Archimedean spiral generator uses a different idea to implement a line. I'll talk about it in the later document. Try to come up with other ideas by yourself. It's also a joy when modeling by programming.