Sunday, 27 October 2013

OpenSCAD - Intro and Example: Designing a filament holder

Over the last few years I have increasingly used OpenSCAD for creating designs of 3D objects. Like many others I was initially put off by the lack of a WYSIWYG point and click interface - it takes time to get used to this scripted method of working.


The OpenSCAD design environment
OpenSCAD - scripted 3D CAD


A common question in forum posts is "why bother", a point and click CAD program is easier to use. For me the advantages come down to these key reasons:

  • Parametrization - Scripted CAD is at the core of the Thingiverse customiser and working with parameters is easy and natural to do when using a programming based approach.
  • Code re-use - Useful elements of designs can be used again and again, as you would with a library of functions in a program. A change to the library can easily be cascaded down to designs which use it.
  • Collaboration - The key advantage, tools like Git can be used to fork and merge the code. Many people can work on the same project in a robust way a method proven in software development.
Like everything its not perfect, I can see the following disadvantages in comparison to a point and click CAD program:
  • Not intuitive to start with - If you are used to learning a program by clicking on stuff to see what happens, as I do for most programs, it can be frustrating.
  • There is no simple way to import or export model files to other non scripted CAD programs other than as an .stl file - fine for 3d printing but not for people who want to use these other tools.
The rest of this (long!) post is a worked example using OpenSCAD to design a filament spool holder for the Lasercut Mendel90. I hope this will help to ease the transition into scripted CAD for those just starting out. This is not meant to replace the manual, or even be a tutorial of all functions. The OpenSCAD documentation is a comprehensive reference and links to a number of tutorials and other worked examples. The final filament holder code is available on github, and its a thing in the thingiverse.

A preview of what the spool holder looks like mounted on the printer:




On to the example!

This worked example assumes you have downloaded and setup OpenSCAD to work on your computer. Load up OpenSCAD and save the new file "filament_holder.scad" somewhere you can find it later.

Filament Spool Holder Requirements

The first step I took was to define the requirements of the spool holder, these boiled down to:
  1. Accommodate spools through the full range of dimensions of those commonly used in 3D printing:
    • Spool width of up to 120mm
    • Spool central internal diameter of ~25mm to >50mm
  2. Accommodate spools weighing up to 3kg
  3. Be very smooth running, if using bearings then either 624 or 608
  4. Be simple, quick to print and attachable to the Lasercut Mendel90 frame with minimum number of fastenings
These requirements led to a long and relatively fruitless thingiverse search - I would rather not have to reinvent the wheel. Nothing met my requirements completely, but the search did confirm the basic form the filament holder would take; a simple peg like arm from the side of the printer. The initial sketch:

Simple filament spool holder concept sketch

I have broken the design process down into a number of steps that make it easier to follow and use in your own designs. there are links within the code examples to the relevant part of the OpenScad documentation so the syntax of each function is not repeated here.

Step 1 - Define the variables

Before starting to design the object, I first defined the variables that will be used for various dimensions. This allows for quick modification later and for easily assigning a new dimension based on existing ones.

//variable defines, all in mm 
//##overall spool dimensions##
max_spool_width=120; 
min_spool_id=25; //id =internal diameter
spool_id_clearance=1; //minimum clearance between the spool holder and the inside of a spool
//##bearing variables## 
bearing_od=13; //624ZZ bearing 
bearing_id=4; //624ZZ bearing 
bearing_thickness=5; 
bearing_washer_thickness=0.5; //M4 washer
bearing_washer_radius=9/2; //M4 washer 
bearing_clearance=1; //clearance between the bearings and the spool holder
//a set of two bearings with a washer at each end as a spacer
bearing_stack_thickness=(2*bearing_thickness)+(2*bearing_washer_thickness); 
bearing_stack_wall_thickness=2; //the printed ends of a bearing stack 
//the bolt to hold the stack and plastic walls together
bearing_stack_bolt_length=bearing_stack_thickness+ bearing_stack_wall_thickness*2+10;
echo("bearing_stack_bolt_length: ", bearing_stack_bolt_length);

I have chosen to echo this information so I know what length of bolt will be required to hold the bearing stack together. The variables continue:

//##filament holder variables##
min_filament_holder_id = bearing_od+bearing_clearance;
max_filament_holder_od = min_spool_id-spool_id_clearance;
filament_holder_wall_thickness= max_filament_holder_od-min_filament_holder_id;
//overall length of the filament holder less the mounting plate
filament_holder_length=max_spool_width+bearing_stack_bolt_length;
echo("filament_holder_length: ", filament_holder_length);

//##mounting plate##
fastener_clearance_radius=3.3/2;
fastener_washer_clearance_radius=8/2;
mounting_plate_thickness=8;
mounting_plate_width=min_spool_id+fastener_washer_clearance_radius;
mounting_plate_height=min_spool_id+fastener_washer_clearance_radius*3;

//##miscellaneous##
fn_resolution=100; //quality vs render time

print_clearance=0.1; //additional clearance between two printed objects.

Step 2 - Put the framework in place.


I find it easier to break a design into logical modules and the first step is to lay out the empty modules as a framework. In this design there is the main tube, the bearing stacks and a mounting plate. Splitting it up into parts makes the design easier to adapt in the future, for example if you wanted to use a different mounting method.

//The "peg" like tube the filament reel will sit on
module main_tube(){
}

//The plate that holds the main tube to the side of the printer
module mounting_plate(){
}

//each bearing stack has two printed ends
module bearing_end(){
}

//The layout for exporting as an .stl file
module spool_holder_stl(){
}

I use OpenSCAD both to design the parts of the object and also to virtually assemble it to confirm it fits together as I imagined it prior to printing it out. To that end I want two output options - one laid out for export as a .stl file and an assembly which shows the filament spool holder with all the parts as they fit together. I adopted this method of working from Nophead's file layout for the Mendel90.

//the printed bearing ends, washers and two bearings
module bearing_stack_assembly(){
}

//assembly to show how it all goes together.
module spool_holder_assembly(){
}

if(1)
spool_holder_assembly();
else
spool_holder_stl();

The final if statement just allows for quickly choosing either to see the assembly (1) or the stl layout (0).


Step 3 - Design the main tube


Starting with the main tube, fill in the framework laid out above.

//The "peg" like tube the filament reel will sit on
module main_tube(){
      //peg
           cylinder(h=filament_holder_length,r=max_filament_holder_od/2,
                                $fn=fn_resolution,center=true);
}

The main element is a cylinder, the length of the filament holder specified in the variables, with a diameter of the maximum filament holder outer diameter.


Filament holder basic tube

cylinder(h=filament_holder_length,r=max_filament_holder_od/2,                                $fn=fn_resolution,center=true);
The $fn resolution setting allows quick renders using a circle with less sides, or to increase resolution for a smoother object for printing. Since we have defined this variable at the beginning we only need to change it there to increase the speed or quality of the render.

The bearings should run inside this cylinder, so the next part is the shape to cut out from the object. The "difference()" function takes all the following parts away from the first part enclosed with the curly braces "{}".
Filament holder bearing cutout



//The "peg" like tube the filament reel will sit on
module main_tube(){
    difference(){
      //peg
           cylinder(h=filament_holder_length,r=max_filament_holder_od/2,
                                $fn=fn_resolution,center=true);

      //cut out for bearings  
      translate([0,(max_filament_holder_od-min_filament_holder_id),0])
        cube([min_spool_id+5,
              min_filament_holder_id/2,
              filament_holder_length-bearing_stack_wall_thickness*2],
              center=true);
      translate([0,(max_filament_holder_od-min_filament_holder_id)/2,0])
        cylinder(h=filament_holder_length-bearing_stack_wall_thickness*2,
                 r=min_filament_holder_id/2,$fn=fn_resolution,center=true);
      translate([0,(max_filament_holder_od-min_filament_holder_id/2)/2,0])
        cube([min_filament_holder_id,
              min_filament_holder_id/2,
              filament_holder_length-bearing_stack_wall_thickness*2],
              center=true);
  }
}

Translate moves the parts around by the amount specified in X, Y and Z respectively. Using variables here means we can put the cutout elements exactly where they are needed in relation to the cylinder and they will scale if we change the variables to fit a different requirement. 


Filament holder main tube with bearing cutouts illustrated

You will notice the "#" in front of each element - that is to make them visible. They are not displayed by default since they are the absence of anything in the space they occupy. Without the "#" debug operators the cylinder with the bearing space cutout is shown


Filament holder main tube with bearing cutouts upright


This cylinder would print fine vertically (even the lip at the top would probably bridge right), but it suffers from a design problem due to the nature of the 3d printing process. Objects are much stronger perpendicular to the layers than parallel to the layers as they can split along layer boundaries. The simple sketch below illustrates this issue:


Shear force failure in 3d printed objects, a simple illustration
3D Printed objects; stronger perpendicular to layers due to layers shearing

Given the weight of the filament reels (max ~3kg) this might not be a problem but it would be better to print this horizontally. To do that we add  "rotate([90,0,0])" before the difference function which causes everything after it to be rotated by 90 degrees about the X axis.


Filament holder main tube with bearing cutouts
rotate([90,0,0])     
    difference(){........

A cylinder on its side is difficult to print without support as the first 1/4 has too flat an angle. To get over this we use a truncated teardrop, an idea which was first used to print horizontal holes without support and became the RepRap logo.

The truncated teardrop is not part of OpenSCAD but it is available in a library written by Nophead for the Mendel90 (and no doubt from many other sources). This exposes one of the most powerful aspects of OpenSCAD - re-using code. With this teardrop library file saved in the same directory as the filament holder file, using it is as simple as adding the use statement at the beginning of the filament_holde.scad file.
use <teardrops.scad>
looking inside the "teardrops.scad" file it has a number of modules, the one we want has the syntax:
module teardrop(h, r, center, truncate = true)
So to use it, change the cylinder line to:

  rotate([0,0,180]) //truncated part facing down
        //use a teardrop to make it printable without support.
        teardrop(filament_holder_length, max_filament_holder_od/2,true,true);

Filament holder main tube complete

Finally the full module code looks like this:

//The "peg" like tube the filament reel will sit on
module main_tube(){
  rotate([90,0,0])
    difference(){
      rotate([0,0,180]) //truncated part facing down
        //use a teardrop to make it printable without support.
        teardrop(filament_holder_length, max_filament_holder_od/2,true,true);

      //cut out for bearings  
      translate([0,(max_filament_holder_od-min_filament_holder_id),0])
        cube([min_spool_id+5,
              min_filament_holder_id/2,
              filament_holder_length-bearing_stack_wall_thickness*2],
              center=true);
      translate([0,(max_filament_holder_od-min_filament_holder_id)/2,0])
        cylinder(h=filament_holder_length-bearing_stack_wall_thickness*2,
                 r=min_filament_holder_id/2,
      translate([0,(max_filament_holder_od-min_filament_holder_id/2)/2,0])
        cube([min_filament_holder_id
              min_filament_holder_id/2,
              filament_holder_length-bearing_stack_wall_thickness*2],
              center=true);
        $fn=fn_resolution,center=true);
    }
}

Step 4 - The mounting plate

The mounting plate should position the filament spool holder on the side of the printer using 4 screws or bolts. It is desirable that a M3 socket cap screw, of a length that is already used in the Mendel90 Lasercut assembly is chosen, M3x16mm and M3x20mm are good candidates.

At its simplest this could be a cube, thick enough to hold the weight of the filament against the screws:


simple filament holder base plate

//The plate that holds the main tube to the side of the printer
module mounting_plate(){
  difference(){
      translate([0,(mounting_plate_width-max_filament_holder_od)/2,
                 -(filament_holder_length+mounting_plate_thickness)/2 +0.01])
          cube([mounting_plate_height
                mounting_plate_width,
                mounting_plate_thickness-3],center=true);
    //bolt holes
    for(i=[-1,1])
      for(j=[-1,1])
        translate([j*mounting_plate_height/2 - j*fastener_washer_clearance_radius,
                i*mounting_plate_width/2 -i*fastener_washer_clearance_radius 
                        + (mounting_plate_width-max_filament_holder_od)/2,
                -(filament_holder_length+mounting_plate_thickness)/2])
        {
          teardrop(100, fastener_clearance_radius,true,true);
          translate([0,0,(mounting_plate_thickness-2)/2+20])
            teardrop(40, fastener_washer_clearance_radius,true,true);
        }
    }
}

A nice way to do the 4 bolt holes is to use two nested "for loops", this allows for the same transformations about the center to be applied with sign changes to space the holes out correctly.

The cube above would probably work but it is a bit basic. Sharp corners can encourage lifting due to plastic shrinkage and smooth transitions look nicer so I made the following changes:

Filament Holder base plate with rounded corners

In order to get the rounded corners I am going to use another of Nophead's libraries from the Mendel90. Place this line at the top of the file next to the other "use" command.

use <utils.scad>

The full code for the bearing holder is below. Three things have changed, firstly the "hull" command is used to make a smooth shape by combining a cylinder and the rounded rectangle. Hull is a powerful way to make complicated objects from simple ones. In this case I have defined a cylinder the same size as the main tube to combine with the rounded rectangle of the mounting plate, this will give a smooth transition from the mounting plate to the main tube.

The rounded rectangle is defined in "utils.scad" and takes the normal arguments for a cube, along with the radius of the corner, in this case I chose 2.  Finally I have cut out the areas where the washers of the fixing screws will go to ensure they hold correctly.

//The plate that holds the main tube to the side of the printer
module mounting_plate(){
  $fn=fn_resolution; //smooth rounded rectangle
  difference(){
    hull(){
      translate([0,(mounting_plate_width-max_filament_holder_od)/2,
                  -(filament_holder_length+mounting_plate_thickness)/2])
        //1 degree rotation to compensate for the distortion due to heavy reels
        rotate([1,0,0])
          rounded_rectangle([mounting_plate_height,
                             mounting_plate_width,
                             mounting_plate_thickness-3],
                             2,center=true); //2=radius of corners
      translate([0,0,
               -(filament_holder_length)/2 +bearing_stack_wall_thickness/2+0.5])
        cylinder(h=1, r=max_filament_holder_od/2, 
              $fn=fn_resolution,center=true);
    }

    //bolt holes
    for(i=[-1,1])
      for(j=[-1,1])
        translate([j*mounting_plate_height/2 - j*fastener_washer_clearance_radius,
                i*mounting_plate_width/2 -i*fastener_washer_clearance_radius 
                        + (mounting_plate_width-max_filament_holder_od)/2,
                -(filament_holder_length+mounting_plate_thickness)/2 +0.01])
        {
          teardrop(100, fastener_clearance_radius,true,true);
          translate([0,0,(mounting_plate_thickness-2)/2+20])
            teardrop(40, fastener_washer_clearance_radius,true,true);
        }
  }

}

The $fn parameter is repeated at the beginning of the file because the rounded rectangle does not have that as an option to pass as an argument. Setting it at the beginning means it applies to all cylinders and circles within that module.

Next the main tube and base plate can be put together as one object


Filament Spool Holder assembled without bearings
//The layout for exporting as an .stl file
module spool_holder_stl(){
  translate([0,0,max_filament_holder_od/2]){
    main_tube();
    rotate([90,0,0])
      translate([0,0,-0.001]) //prevent intersecting faces
        mounting_plate();
  }

}

There is a small translate which is used to ensure the two objects overlap, rather than having co-incident faces which could cause problems with stl export later.

Step 5 - Bearing ends


Each pair of bearings is held between two printed ends. These will hold the bearings slightly away from the tube so they can rotate freely, and also stop the filament roll from sliding off the bearings.

This is a simple short cylinder with a slightly offset hole to achieve the clearance from the filament tube


Bearing end


//each bearing stack has two printed ends
module bearing_end(){
  difference(){
       cylinder(h=bearing_stack_wall_thickness,
                r=min_filament_holder_id/2-print_clearance/2,
                $fn=fn_resolution,center=true);

       translate([bearing_clearance,0,0]) //offset the hole by the bearing clearance
            cylinder(h=bearing_stack_bolt_length+10,
                   r=bearing_id/2+0.15,
                  $ fn=fn_resolution,center=true);
  }
}

In order to prevent the filament holder falling off we want a ridge on the outer bearing ends but not the inner ones. I could make two separate modules, one for each but the advantage of using scripts is that arguments can be passed to them. I have already shown this in using both built in functions and external libraries. In order to have a modules accept an argument insert the name and a default value in the module definition. In this case the bearing_end module expects an edge to be either true or false.


//each bearing stack has two printed ends
module bearing_end(edge=true){
  difference(){
    if(edge) //the bearing end sticks up to prevent the filament roll from coming off
      hull(){
        cylinder(h=bearing_stack_wall_thickness,
                 r=min_filament_holder_id/2-print_clearance/2,
                 $fn=fn_resolution,center=true);
        translate([6,0,0])
             cube([6,6,bearing_stack_wall_thickness],center=true);
       }
     else
       cylinder(h=bearing_stack_wall_thickness,
                r=min_filament_holder_id/2-print_clearance/2,
                $fn=fn_resolution,center=true);

       translate([bearing_clearance,0,0]) //offset the hole by the bearing clearance
           cylinder(h=bearing_stack_bolt_length+10,
               r=bearing_id/2+0.15,
               $fn=fn_resolution,center=true);
  }
}

Calling bearing_end(false) gives the same result as before, but bearing_end(true) gives this object:


Bearing end with edge



Two of each end are needed so they are added to the required output for printing.




//The layout for exporting as an .stl file
module spool_holder_stl(){
  translate([0,0,max_filament_holder_od/2]){
    main_tube();
    rotate([90,0,0])
      translate([0,0,-0.001]) //prevent intersecting faces
        mounting_plate();
  }
  for(i=[-1,1])
    translate([i*(max_filament_holder_od+min_filament_holder_id)/2,
              0,bearing_stack_wall_thickness/2]){
       bearing_end(false);
    translate([0,min_filament_holder_id+2,0])
       rotate([0,0,90])
         bearing_end(true);
   }

}

Step 6 - Modelling


At this point all the individual elements of the design are complete. Before printing however it would be good to put it all together as confirmation that it fits together.

First the bearing stacks are assembled




//the printed bearing ends, washers and two bearings
module bearing_stack_assembly(){
  bearing_end(false);
    translate([0,0,bearing_stack_wall_thickness+
              bearing_washer_thickness*2+bearing_thickness*2])
  bearing_end(true);
  //Bearings and washers
  %translate([bearing_clearance,0,bearing_stack_wall_thickness/2+
             bearing_washer_thickness+bearing_thickness])
    difference(){
        union(){
          for(i=[-1,1])
            translate([0,0,i*bearing_thickness+i*bearing_washer_thickness/2])
              cylinder(h=bearing_washer_thickness,
                       r=bearing_washer_radius,
                       $fn=fn_resolution,center=true);
              cylinder(h=bearing_thickness*2,
                       r=bearing_od/2,
                       $fn=fn_resolution,center=true);
         }
         cylinder(h=bearing_stack_thickness+5,
                  r=bearing_id/2,
                  $fn=fn_resolution,center=true);
  }
}

Note the "%" background modifier at the beginning of the bearings section. This displays that part as transparent - useful for quickly visualising areas.

Putting it all together:

Filament spool holder full assembly


//assembly to show how it all goes together.
module spool_holder_assembly(){
main_tube();
rotate([90,0,0]){
mounting_plate();
translate([0,(max_filament_holder_od-min_filament_holder_id)/2,
                          -(max_spool_width/2- (bearing_stack_wall_thickness+
                           bearing_washer_thickness*2 +bearing_thickness*2))])
rotate([0,180,-90])
bearing_stack_assembly();
translate([0,(max_filament_holder_od-min_filament_holder_id)/2,
                          (max_spool_width/2- (bearing_stack_wall_thickness+
                           bearing_washer_thickness*2 +bearing_thickness*2))])
rotate([0,0,90])
bearing_stack_assembly();
}
}


Printing


I used ABS with 2 perimeters  0.4mm layers and 50% infill which makes the holder strong enough for the 2.3kg reels we use.

Other than the printed parts you need,
For the bearings:
4 624ZZ bearings, 4 M4 washers, 2 M4x30mm bolts and 2 M4 nyloc nuts

To mount:
4 M3x20mm bolts, 4 M3 washers, 4 M3 nyloc nuts.

Variations


The first version I printed was for the large 2.3kg reels we use for production printing:

Mendel90 Lasercut Filament Spool holder printed and mounted
Filament Spool Holder printed out and mounted on the Lasercut Mendel90 

Mendel90 Lasercut Filament Spool holder with large spool of filament
The Filament Spool Holder with a large reel (2.3kg) of 3mm filament

The advantage of making the filament holder parametric though is that only one parameter needs changing:

max_spool_width=50; 

To get a shorter tube for smaller rolls of filament:
Shorter version of Filament spool holder

Here are some pictures of it mounted on my test printer. For these smaller rolls the bearings are not actually required and this becomes a rather over engineered peg.

Mendel90 Lasercut, short filament spool holder

Mendel90 Lasercut, dual filament reels


The complete source code is available on githubI hope this has been useful, please leave your feedback and suggestions for improvement in the comments!

2 comments:

  1. Well written Tony, this must have taken a long time to put together. Definitely something which I will take the time to work through - I tried OpenSCAD once but I found it unnerving to draw by programming and quickly got frustrated. Top marks.
    Alan

    ReplyDelete
    Replies
    1. Yeah it took a bit of time to "get it" as far as defining the parameters and coding the shape. I clicked for me with a more complex design; changing a dimension and the whole design updates across multiple objects - pretty cool!

      Delete