Quantcast
Channel: 3D printer improvements
Viewing all articles
Browse latest Browse all 95

How to use Openscad: variables and modules for parametric designs

$
0
0

Part 2/3: Variable and parametric design

So far we used only "hard coded" numbers (see the previous part regarding the basics of Openscad).
Hard coded numbers are mostly always bad. Seriously: when you use Openscad you probably want NOT to use hard coded numbers, but variables instead, as you get lots of advantages in doing so.

A larger mug by using the scale operator (from the "basic" tutorial).
This is still not parametric, as numbers are hard-coded (a bad practice).
For example, the mug above has lots of annoying dependencies: whenever the cube is made taller, the sphere and cylinder parameters must also be updated accordingly.





Better set these number in variables and do some basic math.

A variable is just an alphanumeric tag, usually composed of a set of letters that mean something, and optional underscores (no space is allowed in the name, and numbers may be used but not at the first place).

A variable can be assigned a value, and that can be then used also as if it was a number. There are two advantages to use variables: readability and convenience: set the real number in one place only and never forget to fix other places where the physical value should be used:

  wall_thickness=2;

It is much easier to read a source code that uses variables as you can give meaningful names to them: this "wall_thickness" makes more sense than a number buried in a mathematical expression!

Rewrite the source to be parametric (dependent on variables)

So we are back to our mug, to make it parametric. We also want to drop the global "scale( )" operator because it is quite annoying: since it changes all subsequent axis geometry,  nothing will be "square" anymore so nasty reverse projections would become needed each time we want to use primitives. Better try to use "scale( )" it sparingly and only when required! From now on, we apply it only on a primitive, the sphere, to elongate it vertically so it matches the cuboid (not really a square cube!).

  width=40;
  height=60;
  bottom_thickness=2;
  wall_thickness=5;

  difference()
  {
    intersection()
    {
      cube([width,width,height], center=true);
      scale([1,1,height/width]) // so that Z=1 is now...
        sphere(width/2 * sqrt(2)); // close to the cuboid edges
    }
    translate([0,0,-height/2+bottom_thickness])
      cylinder(r=width/2-wall_thickness,h=height+0.1);
  }


A "good" Openscad source code should almost have no numerical values. Most of the internal stuff is computed out of a minimum set of variables that make sense in the design.

Note: so far, better consider Openscad variables as constants. They can be defined only once at the top of the file or at the top of a module, and not re-defined afterwards.

As shown below, we now generate a new mug or an ashtray just by changing the width and height variables to 40 and 60 mm respectively, or 80 and 20 mm (well, a plastic ashtray is not a good idea).


Two "mugs" made from the same source code: only two numbers need to be changed (height and width)
Parametric designs are probably one of the most important characteristics of Openscad.


Now it still can be made better. Imagine we would like to create a mug with an attached cup below to collect the coffee that overflows. Both the mug and the cup are made out of the above shape.

Export/import STL files

Sure, we could generate the mug, export it to an STL file, and then re-import the STL in our modified source code. Indeed, Openscad knows how to import raw STL files, which is convenient to tweak an existing design for which no source is given (or not compatible with Openscad).

Here is how it would look like here:

A nasty way to import a parametric copy of ourselves, with the "import" function of Openscad and after
the first design was exported as STL. But this is not really the right way to do as we do know the source code!

In fact this was not a proper way to deal with the thing, even if we omit the fact we have a rogue "60" in the source code (the translation of the "import( )" statement, which itself depends on the original size of the mug).

So shall we copy/paste the mug code to get two versions-in-one? Obviously not.

For once, copying/pasting source code is wrong because any bug left in the copied source will be duplicated, and also any subsequent improvement need to be re-inserted in the many copies, that each tend to diverge with time from each other.

Writing a module: recyclable and parametric shapes

Instead of copy/pasting source code, Openscad offers modules that are the right choice here.

The idea of a module is a "functional" shape that accepts a certain number of parameters, just like the primitive shapes do. In fact we are going to create another shape that can be used just like any of these primitive shapes.

Here is how we have to modify our source code:

  module mug(width, height, bottom_thickness=2, wall_thickness=5)
  {
    difference()
    {
      intersection()
      {
        cube([width,width,height], center=true);
        scale([1,1,height/width])
          sphere(width/2 * sqrt(2));
      }
      translate([0,0,-height/2+bottom_thickness])
        cylinder(r=width/2-wall_thickness,h=height+0.1);
    }
  }

This module is there to generate new mug shape (just like cube or cylinder create their respective shapes, but our mug is certainly no more a primitive one). This "mug" shape is created with up to four parameters, that we had previously introduced with the notion of variables.

You may have noticed that two of these parameters have default values, which means they are not compulsory when we create a new mug. This is quite convenient because we probably do not care much about the thickness as long as they are OK. Still, if you give them a value it will be used instead of the default.

Now if you try to run the above, you get nothing new nor updated. In fact the Openscad console tells this:
   ERROR: CSG generation failed! (no top level object found)
This is one more reminder that you should better always leave the console open, or you will not always understand why nothing appears or where something is broken.

The error means "nothing is left to be drawn at the end of the code". And this is true since we just have defined the mug shape, but we created none so far.

So to create our original mug, we would now add the following line, which must be placed outside of the module, for example at the end of the file:

  mug(width=40, height=60);

For the cup/ashtray, it would just be another set of values:

  mug(width=80, height=20);

See how powerful Openscad is becoming? And there are more to come...

Hence, our combined/dual shape can be made by creating two shapes thanks to our module.

By the way, you will see that I also corrected the vertical offset of the mug shape, so that Z=0 corresponds to the bottom of the shape. Nobody wants that the mug is positioned at its middle, as it makes life harder for callers of the mug shape.

You may also notice that no curly braces were used for the added "translate" at the top of the mug module. This is fully OK as there is only one shape that needs to be translated after, namely, the (result of the) "difference" that follows.
Our coupled mug and teacup thanks to a module that builds generic (aka parametric) mug shapes.


  module mug(width, height, bottom_thickness=2, wall_thickness=5)
  {
    translate([0,0,height/2])
      difference()
      {
        intersection()
        {
          cube([width,width,height], center=true);
          scale([1,1,height/width])
            sphere(width/2 * sqrt(2));
        }
        translate([0,0,-height/2+bottom_thickness])
          cylinder(r=width/2-wall_thickness,h=height+0.1);
      }
  }

  // here is the real objects that are made
  union()
  {
    mug(width=40, height=60); // one mug shape1
    mug(width=80, height=20); // another one, different
  }

We can also create and use convenient "local" variables in the module. See below? We added the explicit "r_of_inside". This variable will not be seen at all from the outside of the module, and even though it is used only once at the end of the module, it helps improve readability. Note also that I moved the translation by height/2 closer to the "positive" shape. Carving the cylinder is more easily read without a centering.

  module mug(width, height, bottom_thickness=2, wall_thickness=5)
  {
    r_of_inside=width/2-wall_thickness;
    difference()
    {
      translate([0,0,height/2])
        intersection()
        {
          cube([width,width,height], center=true);
          scale([1,1,height/width])
            sphere(width/2 * sqrt(2));
        }
      translate([0,0,bottom_thickness])
        cylinder(r=r_of_inside,h=height+0.1);
    }
  }

Important note: there are weird restrictions related to variables in Openscad. They recently leveraged a few of them (most notably to allow recursion, i.e. a module calling itself will no fail having separate values). And even though it seems to lack some of these late improvements, I suggest reading this more advanced presentation of variables when you need it, written by +Stephanie Shaltes who regularly post about advanced subjects in Openscad.

Recycling the modules from other Openscad source files

What is nice now, is that you can save the mug module to a scad file and import it and use it as if it was a primitive shape.

To do so you can create a new scad file and tell it to import your former mug scad file. I have to admit that the syntax is stupid given that "include" works as a regular function, why does it need to be otherwise for this one?


  include

Now there is a special comment to do. The above really behaves as if you had copy/pasted the scad source from "mug.scad" in place of the "include" statement. So you will see the mug-with-a-cup that was defined above.

There is another, probably more useful may to recycle your old file, by using another function like this:

  use 
  mug(width=40, height=60);

This time, the "use" statement only imports the definition, including the modules, that were defined in "mug.scad". It will just not execute anything else. So the "use" statement generates no shapes on its own, even though there may be some in "mug.scad" (as we have). This is quite convenient because it leaves the possibility to have "mug.scad" do something on its own (e.g. just to show working examples as we did), without polluting the new code here.

Note: there are a lot of almost-official modules, in a library named MCAD. It brings many shapes and standard nuts, bolts and other industrial equipment. It is free just as much as Openscad, and the source code is here.

Convex hull: creating complex shapes with basic ones.

Now, life with Openscad would not be possible with one more operator (at least): the convex hull.
Think of it as the shape that you get when you wrap a plastic around a bunch of shapes with no resulting concave parts.

A sample will make this clear:
  hull()
  {
    cube([10,10,10], center=true);
    translate([20,0,0]) sphere(r=3);
  }

Below you get an animation that changes from a regular "union( )" to a "hull( )".
Animation showing the effect of "hull( )", aka "convex hull".

Usually no "complex" shape is used within a hull. But there are often more than two primitives. here is one case: to build a cube with round corners for example.
Note: there are many way to build such a shape, and using a hull is somehow overkill: a union is enough, as we will see after. Anyhow, this is just an illustration of the use of hulls.

  hull()
  {
    translate([-10,-10,0])
      cylinder(r=8,h=50, center=true);
    translate([10,-10,0])
      cylinder(r=8,h=50, center=true);
    translate([-10,10,0])
      cylinder(r=8,h=50, center=true);
    translate([10,10,0])
      cylinder(r=8,h=50, center=true);
  }

Do not forget that you can prefix a shape with % or # when you want to check its individual contribution to the union!

One way to build a (partially) rounded cube.
We will do this better as we will see!

There are two improvements to do here. First, a hull is a lengthy operation, where Openscad need to compute a lots of stuff. It is usually kept for shapes that cannot be achieved otherwise.

Here, the above is much faster with a simple union of 4 cylinders and 2 cuboids, this way:

  union()
  {
    translate([-10,-10,0]) cylinder(r=8,h=50, center=true);
    translate([10,-10,0])  cylinder(r=8,h=50, center=true);
    translate([-10,10,0])  cylinder(r=8,h=50, center=true);
    translate([10,10,0])   cylinder(r=8,h=50, center=true);
    cube([20,20+2*8,50], center=true); // space left inside
    cube([20+2*8,20,50], center=true); // and in the other way
  }

Then, a second improvement can be made because this is not a proper way to code anyway. There are lots of copy/pasted code as you can see, that ought to be better made with loops.

This is the subject of the third article, about advanced CSG techniques in Openscad (in progress).


Viewing all articles
Browse latest Browse all 95

Trending Articles