Wednesday, March 30, 2016

Joining two circles/cylinders in openscad 2: Code and Pretty pictures

In an previous we developed the equations to make a nice curve to join two circles (or cylinders). Note that can be used anywhere, including in 3D drawing. I will cheat by using a 3D modelling program that allows me to enter the equation in some kind of programmatic way, it is open source, and can be run in the usual operating systems. If you read the title of this article, you probably figured out by now we are going to use OpenSCAD. And you would be right.

Openscad differs from most CAD programs in that you do not draw interactively using a GUI. Instead you describe the objects using a programming language. Let me show what I mean using a really basic example which I stole from the manual:

module test1()
{
        cube([2,3,4]);
}

test1();

which will draw a cube of sides 2, 3, and 4 (new definition of the word "cube" we were not aware of; just roll with it). The files have a .scad extension by default, so I named this test file test1.scad. The main "entity" is a module. Inside a module you do your 2D or 3D drawings. You can have modules inside modules and modules calling other modules that are somewhere else. And then you need to finally call the module that calls everything else so something is drawn (we call it rendered). We can also define functions and variables (most of which behave like constants). Instead of keeping making new examples, which the online manual already has plenty, let's say we want to design some kind of hinge thingie that looks like this (gaze upon the magnificence of this engineering sketch):

We will develop the file that describe such 3D object as we go.

Since we are making a hinge, I will call the file hinge.scad. This is how a completely barebones version of the file looks like:

module hinge()
{
}

hinge();

Not much going on, but at least it will run. I will assume all my units are in mm. And I will say the two holes are 10mm and 15mm in diameter. And the hinge thickness is 2mm, or 20% of the smaller circle's diameter. Putting all of that in our little file, we have

/**********************************************************************
 * Let's make a stylish door hinge, shall we?
 *
 * NOTEs:
 * o Units are in mm
 **********************************************************************/
module hinge()
{
   // Known quantities
   hole1_dia = 10;
   hole2_dia = 15;
   hinge_thickness = hole1_dia*.2;
}

hinge();

The hole name comes from we are deciding how big the hole for the pins/bolts that will hold the hinge to the frame and door need to be. I am throwing some numbers here, but in real life those are known quantities (1: grab bolt. 2: measure its diameter). The hinge thickness is how much meat will be around those bolts so the hinge will not collapse like a bunch of broccoli. Of course the minimum thickness is material dependent.

With that said, here are some interesting things to note in the file so far:

  1. Comments are very similar to C/C++
  2. Statements end with a ;
  3. hinge_thickness may seem to be a variable but it is evaluated when the file is compiled, not when it is run.

How does the file look so far in openscad? Let's load it and see:

Note we have not drawn anything yet, so there is nothing to be seen. How about if we make those two circles? Because of the simple shape of the hinge, we can do the hard work in 2D and then extrude it. This is also a good time to decide the distance between the two holes; I'll say their centers are 32mm apart, which means the circles are 30 -7 -5 = 18mm apart, and that distance henceforth shall be named circle1_height. Let's update the code and tell it to dry the two circles:

/**********************************************************************
 * Let's make a stylish door hinge, shall we?
 *
 * NOTEs:
 * o Units are in mm
 **********************************************************************/        
module hinge()                                                                  {                                                                                  
   // Known quantities                                                             
   hole1_dia = 10;
   hole2_dia = 15;
   hinge_thickness = hole1_dia*.2;
   circle1_height = 30;

   module body()
   {
      // circle2 will be located at the origin
      circle( r=(hole2_dia + 2*hinge_thickness)/2 );                              
      // circle1 will be located at y = circle1_height from circle2
      translate([0, circle1_height, 0])                                               
      circle( r=(hole1_dia + 2*hinge_thickness)/2 );
   }

   body();
}

hinge();

So, the body starts with the two circles (we have not drilled the holes for the bolts yet), where circle2 is drawn on the center of the coordinate system (coordinate [0,0,0]) and circle1 is drawn by first moving to coordinate [0,circle1_height,0] (moving up on Y axis of circle1_height and then drawing it. It does feel like you are telling it to move an imaginary 3D plotter head around to do your drawings.

And here is how it looks like on the screen. Openscad is representing the 2D objects in 3D by giving them a thickness of one unit.

Making progress; now we need to connect the two to get the general shape of the hinge.

The Lazy Way

Do you remember the K.I.S.S. (Keep It Simple, St.. er Oh Brave Enterprising One)? It is really a variation of being lazy: what is the least amount of work you have to do to get the job done? For our case, what would be the simplest way to connect the two circles? Two points define a line, so if we use a point on each circle we can draw a line between them. Mirror that and we have enclosed the mess. We will need an easy point on each circle, I do not want to think too hard so here is my proposal: if we draw a line connecting the centers of the two circles, we can then draw the diameters perpendicular to this line. Where these two lines intersect each circle define two points as shown in the picture on the right, which we stole from a previous article.

The way we will create the region connecting the two circles is by creating a trapezoid and then joining it to the circles. This will make more sense in when you see it. So, let's modify the code and introduce a command called polygon().

/**********************************************************************
 * Let's make a stylish door hinge, shall we?
 *
 * NOTEs:
 * o Units are in mm
 **********************************************************************/        
module hinge()                                                                  {                                                                                  
   // Known quantities                                                             
   hole1_dia = 10;
   hole2_dia = 15;
   hinge_thickness = hole1_dia*.2;
   circle1_height = 30;

   module body()
   {
      // Create the trapezoid
      module fillerStuff()
      {
         polygon(points=[[-(hole2_dia + 2*hinge_thickness)/2,0],[-(hole1_dia + 2*hinge_thickness)/2,circle1_height], [(hole1_dia + 2*hinge_thickness)/2,circle1_height], [(hole2_dia + 2*hinge_thickness)/2,0]], paths=[[0,1,2,3]]);
      }
 
     // circle2 will be located at the origin
      circle( r=(hole2_dia + 2*hinge_thickness)/2 );                              
      // circle1 will be located at y = circle1_height from circle2
      translate([0, circle1_height, 0])                                               
      circle( r=(hole1_dia + 2*hinge_thickness)/2 );

      // Draw the trapezoid
      fillerStuff();
   }

   body();
}

hinge();

Let's see how it looks like:

Maybe a top view?

It is hard to notice from these pretty pictures, but one issue our code is that it is drawing 3 different objects on the screen instead of one. If all you want to do is draw something on the screen, this might sound pedantic. After all, it looks good so far. But we are approaching this 3D drawing business from a programming standpoint. And it is good programming practice to use objects made of smaller objects as needed. As in Java (gasp!) C++ (Ugh!) and Python (Irk!), once the object is defined, we can manipulate this single object. For instance, we can apply transformations -- resize, distort, move, etc -- to one single object instead of each of its components. And, OpenSCAD is smart enough not to draw the areas common to the parts that make this object. OpenSCAD calls a module() what we would call an object; I think by now you have noticed the module() is the main entity in this program.

Enough of this boring talk, let's see how we do it. So we need to join them using union(). And while we are at it, let's show the 3 different objects that make this union by adding a #, the debug modifier to the command used to draw the trapezoid:

/**********************************************************************
 * Let's make a stylish door hinge, shall we?
 *
 * NOTEs:
 * o Units are in mm
 **********************************************************************/        
module hinge()                                                                  {                                                                                  
   // Known quantities                                                             
   hole1_dia = 10;
   hole2_dia = 15;
   hinge_thickness = hole1_dia*.2;
   circle1_height = 30;

   module body()
   {
      // Create the trapezoid
      module fillerStuff()
      {
         polygon(points=[[-(hole2_dia + 2*hinge_thickness)/2,0],[-(hole1_dia + 2*hinge_thickness)/2,circle1_height], [(hole1_dia + 2*hinge_thickness)/2,circle1_height], [(hole2_dia + 2*hinge_thickness)/2,0]], paths=[[0,1,2,3]]);
      }
 
      union()
      {
         // circle2 will be located at the origin
         circle( r=(hole2_dia + 2*hinge_thickness)/2 );

         // circle1 will be located at y = circle1_height from circle2
         translate([0, circle1_height, 0])
         circle( r=(hole1_dia + 2*hinge_thickness)/2 );

         // Draw the trapezoid
         # fillerStuff();
      }
   }

   body();
}

hinge();

And here is the output. The view is still the same, but now we can clearly see the fillerStuff() module compared to the two circles.

Looks like everything is nice and rosy. Or rosy and yellow.

Or, are they?

Tangents anyone?

Our lazy way would have worked perfectly if both circles had the same diameter. But they are not. I know the last few pictures look very nice, but let me see if I can show you the issue by changing circle1_height to, say, 20:

You might have to click on the picture to see it, but the intersection between the line and the small circle looks very wrong because they are at different angles.

Er, it doesn't. I think we will need to crank up the resolution a bit by setting $fn=100.

/**********************************************************************
 * Let's make a stylish door hinge, shall we?
 *
 * NOTEs:
 * o Units are in mm
 **********************************************************************/
module hinge()
{
   // Known quantities
   hole1_dia = 10;
   hole2_dia = 15;
   hinge_thickness = hole1_dia*.2;
   circle1_height = 15; // Y-axis
   $fn=100; // crank up the resolution
[...]

That will make rendering slow but should smooth things out. I also made circle1_height even smaller (15), to the point the two circles touch themselves (bonus point: why do they are more than just touching each other?) and make where the circles touch the line even more dramatic:

That will only look nice if the line is tangent to the circle. Since said line touches both circles, it must be tangent to both of them. What we are looking for is the external common tangent, whose explanation and methods of obtaining are described in many locations; I used this one. Let's modify the code to calculate the points in the two circles that satisfy that and then draw the trapezoid around them; I will try later on to talk about how to obtain it as applied to OpenSCAD.

For now we need to know how to do asin(), functions.

Here is how the code looks like. I modified it a bit so the 4 points used to make the trapezoid are calculated before it is drawn.

/**********************************************************************
 * Let's make a stylish door hinge, shall we?
 *
 * NOTEs:
 * o Units are in mm
 **********************************************************************/        
module hinge()                                                                  {                                                                                  
   hole1_dia = 10; // Top circle
   hole2_dia = 15; // Bottom circle
   hinge_thickness = hole1_dia*.2;
   circle1_height = 15; // Y-axis
   $fn=100; // crank up the resolution

   // Useful functions
   function geta1(r1, r2, k) = k*r2/(r2 - r1);
   function getAlpha(opposite_side = 1, hypothenuse = 1) =
                     asin(opposite_side/hypothenuse);

   module body()
   {
      // Create the trapezoid
      module fillerStuff()
      {
         /************** Lazy
         point0 = [-(hole2_dia + 2*hinge_thickness)/2, 0];
         point1 = [-(hole1_dia + 2*hinge_thickness)/2, circle1_height];
         point2 = [(hole1_dia + 2*hinge_thickness)/2, circle1_height];
         point3 = [(hole2_dia + 2*hinge_thickness)/2,0];
          *************/

         r1 = (hole1_dia + 2*hinge_thickness)/2;
         r2 = (hole2_dia + 2*hinge_thickness)/2;
         a1 = geta1(r1, r2, circle1_height);
         alpha = getAlpha(r1, a1);
         echo("Opposite side for small triangle: ", a1);
         echo("Angle in degrees: ", alpha);
         y1 = circle1_height + r1*sin(alpha);
         y2 = r2*sin(alpha);
         x1 = r1*cos(alpha);
         x2 = r2*cos(alpha);
         echo(x1=x1, y1=y1, x2=x2, y2=y2);

         // Points that make the trapezoid
         point0 = [-x2, y2];
         point1 = [-x1, y1];
         point2 = [x1, y1];
         point3 = [x2, y2];

         polygon(points=[point0, point1, point2, point3], paths=[[0,1,2,3]]);
      }
 
      union()
      {
         // circle2 will be located at the origin
         circle( r=(hole2_dia + 2*hinge_thickness)/2 );

         // circle1 will be located at y = circle1_height from circle2
         translate([0, circle1_height, 0])
         circle( r=(hole1_dia + 2*hinge_thickness)/2 );

         // Draw the trapezoid
         # fillerStuff();
      }
   }

   body();
}

hinge();

We are also using the echo function to print out to the console some intermediate values. I used that to debug my code, which was wrong on the first trial and forced me to redraw the image so I could get the right equations. Here is how this output looks like:

Compiling design (CSG Tree generation)...
ECHO: "Opposite side for small triangle: ", 57
ECHO: "Angle in degrees: ", 7.05413
ECHO: x1 = 6.94701, y1 = 15.8596, x2 = 9.42809, y2 = 1.16667
Compiling design (CSG Products generation)...
Geometries in cache: 44
Geometry cache size in bytes: 154304
CGAL Polyhedrons in cache: 0
CGAL cache size in bytes: 0
Compiling design (CSG Products normalization)...
Compiling highlights (1 CSG Trees)...
Normalized CSG tree has 2 elements
Compile and preview finished.
Total rendering time: 0 hours, 0 minutes, 0 seconds

The ECHO statements are the output of the echo commands we inserted in the code. They are only printed to the console window. So, did our extra coding effort paid off? Here is the image with the new trapezoid nodes.

Note the transition between circles and straight lines look much nicer.

A Touch of Flair

I think we are making a lot of progress: we have a hinge that looks very smooth. But, let's not rest on our laurels just yet. I think we can do it even nicer, and get really close to the first picture in this article. And we shall do so by modifying our code to use the equations we created in a previous 3D drawing article. Note that once again we commented out the lines for the previous joining solution, tangents, so they are in the same code.

/**********************************************************************
 * Let's make a stylish door hinge, shall we?
 *
 * NOTEs:
 * o Units are in mm
 **********************************************************************/        
module hinge()                                                                  {                                                                                  
   hole1_dia = 10; // Top circle
   hole2_dia = 15; // Bottom circle
   hinge_thickness = hole1_dia*.2;
   circle1_height = 15; // Y-axis
   $fn=100; // crank up the resolution
   cutout_dia = 50;

   // Useful functions
   function geta1(r1, r2, k) = k*r2/(r2 - r1);
   function getAlpha(opposite_side = 1, hypothenuse = 1) =
                     asin(opposite_side/hypothenuse);
   // Fancy curves
   function getC2(a=1, b=1, c=1) = (pow(a,2) + pow(c,2) - pow(b,2))/(2*c);
   function getCx0(hip=1, side=1) = sqrt(pow(hip,2) - pow(side,2));

   module body()
   {
      // Create the trapezoid
      module fillerStuff()
      {
         /************** Lazy
         point0 = [-(hole2_dia + 2*hinge_thickness)/2, 0];
         point1 = [-(hole1_dia + 2*hinge_thickness)/2, circle1_height];
         point2 = [(hole1_dia + 2*hinge_thickness)/2, circle1_height];
         point3 = [(hole2_dia + 2*hinge_thickness)/2,0];
          *************/
         r1 = (hole1_dia + 2*hinge_thickness)/2;
         r2 = (hole2_dia + 2*hinge_thickness)/2;
         /************** Tangent
         a1 = geta1(r1, r2, circle1_height);
         alpha = getAlpha(r1, a1);
         echo("Opposite side for small triangle: ", a1);
         echo("Angle in degrees: ", alpha);
         y1 = circle1_height + r1*sin(alpha);
         y2 = r2*sin(alpha);
         x1 = r1*cos(alpha);
         x2 = r2*cos(alpha);
         echo(x1=x1, y1=y1, x2=x2, y2=y2);
          *************/

         a = (2*r2 + cutout_dia)/2;
         b = (2*r1 + cutout_dia)/2;
         c = circle1_height;
         c2 = getC2(a, b, c);  // Projection of a onto c (y axis)
         c1 = c - c2;   // Projection of b onto c (y axis)
         Cx0 = getCx0(a, c2);
         C0 = [Cx0, c2]; // Tangent circle center
         C1 = [-Cx0, c2]; // Tangent circle center
         // Tangent points as coordinates
         x2 = Cx0*r2/a;
         y2 = c2*r2/a;
         x1 = Cx0*r1/b;
         y1 = c - c1*r1/b;

         // Points that make the trapezoid
         point0 = [-x2, y2];
         point1 = [-x1, y1];
         point2 = [x1, y1];
         point3 = [x2, y2];

         // Draw it
         difference()
         {
            polygon(points=[point0, point1, point2, point3], paths=[[0,1,2,3]]);
            translate(C0)circle(r=cutout_dia/2, center=true);
            translate(C1)circle(r=cutout_dia/2, center=true);
         }
      }
 
      union()
      {
         // circle2 will be located at the origin
         circle( r=(hole2_dia + 2*hinge_thickness)/2 );

         // circle1 will be located at y = circle1_height from circle2
         translate([0, circle1_height, 0])
         circle( r=(hole1_dia + 2*hinge_thickness)/2 );

         // Draw the trapezoid
         # fillerStuff();
      }
   }

   body();
}

hinge();

Fun fact: If you are using a PLA filament 3D printer, having the outside sides of the trapezoid be curved will look nicer than going from circle to straight line. I was told it is because the printer needs to stop when going from curve to straight. But we can talk about printing our handiwork in another article.

You may have also noticed how progressively more complex the code has become. But that is a small price to pay for beauty. And now let's see what all that effort brings us

Not bad. What if we increase circle1_height to, say, 20?

Looks very smooth. Now we then take the # from

// Draw the trapezoid
         # fillerStuff();

Er, what about the holes for the pins/bolts?

Good catch! I forgot completely about them! So, how about if we change

body();

to

module body_hole()
   {
      union()
      {
         // circle2 will be located at the origin
         circle( r=hole2_dia/2 );

         // circle1 will be located at y = circle1_height from circle2
         translate([0, circle1_height, 0])
         circle( r=hole1_dia/2 );
      }
   }

   difference()
   {
      body();
      body_hole();
   }

Let's see how it looks like

A nice angled view perhaps?

And, finally, let's give some thickness to this hinge by using the linear_extrude() module (I am cheating and saying the hinge is circle1_height thick in the code excerpt below).

module body_hole()
   {
      union()
      {
         // circle2 will be located at the origin
         circle( r=hole2_dia/2 );

         // circle1 will be located at y = circle1_height from circle2
         translate([0, circle1_height, 0])
         circle( r=hole1_dia/2 );
      }
   }

   linear_extrude(height = circle1_height, center = false, convexity = 10)
   difference()
   {
      body();
      body_hole();
   }

And look at it

I think it looks very nice, how about you? If you agree with me, I think we succeeded into doing a quick intro to using OpenSCAD. Ok, it is neither quick nor really intro, for I rushed a bit in some of the equations. The ones for the fancy hinge were derived in a previous article, but that is not the case for the tangential solution. How about this: I will make later on a new article showing how those equations were derived. And then, we will do some 3D printing to see how our handiwork looks! Sounds like a plan?

1 comment:

Yash said...

Hey, this post really helped me out with an OpenSCAD project I'm working on. I honestly don't understand much trigonometry. I changed the coordinates around because the two circles to connect are along the x axis instead of y. But I was wondering if there was a way to automate the generation of curved tangents regardless of the orientation of the circles?