Makertinesday

February 14, 2014 Additive Manufacturing

This Valentines Day I decided to give something a shot that I’ve been wanting to try for a while… CNC lino-cut printing.  Now, I’m sure that some will feel that doing the cutting with CNC removes some of the craft involved… and you’re right.  Honestly, I’ve tried doing the manual version, and it made me really appreciate when I see a nice lino-cut print… because it is really hard work.

Anyways, I also wanted to work some other practice into it, so I decided to make my design generatively, using Processing.  Since I’ve been really into facets and mesh recently, I decided to play around with the Voronoi function in the Toxiclibs library by Karsten Schmidt.  The function uses Delaunay Triangulation to turn a point cloud into a triangle mesh, maximizing the minimum angle for all of the triangles.  Using interpolated points along some Bezier curves, I defined points in a heart shape, as well as the border that defines it.  I also used a Point in Polygon algorithm to selectively display the triangles outside of the heart, but inside the box, as well as create the rest of the point field.  At first, I tried randomizing the points to get a more interesting mesh.  However, what it produced looked pretty sloppy.  So, I created a more symmetrical version, by scaling up the heart shape and generating more points using layers around the original heart shape.

  • Writing code!

Here is the code:

import toxi.geom.*;
import toxi.geom.mesh2d.*;

import toxi.util.*;
import toxi.util.datatypes.*;

import toxi.processing.*;

import processing.dxf.*;

boolean record;
ArrayList<PVector> vertices;
ArrayList<PVector> heart;
ArrayList<PVector> bigheart;
ArrayList<PVector> bigheart2;
ArrayList<PVector> box;

// radius of the root triangle which encompasses (MUST) all other points
float DSIZE = 10000;

// a Voronoi diagram relies on a Delaunay triangulation behind the scenes
// we simply use this as a front end
Voronoi voronoi;

void makeHeart(){
   noFill();
   int[][] bezPts = new int[4][2];
   bezPts[0][0] = width/2;
   bezPts[0][1] = height/2-height/5;

   bezPts[1][0] = width/2+3*width/12;
   bezPts[1][1] = height/2-7*height/16;

   bezPts[2][0] = width/2+width/3;
   bezPts[2][1] = height/2+height/16;

   bezPts[3][0] = width/2;
   bezPts[3][1] = height/2+height/4;

  //bezier(bezPts[0][0],bezPts[0][1],bezPts[1][0],bezPts[1][1],bezPts[2][0],bezPts[2][1],bezPts[3][0],bezPts[3][1]);
  fill(255);
  int steps = 8;
  vertices = new ArrayList<PVector>();
  heart = new ArrayList<PVector>();
  for (int i = 0; i <= steps; i++) {
    float t = i / float(steps);
    float x = bezierPoint(bezPts[0][0], bezPts[1][0], bezPts[2][0], bezPts[3][0], t);
    float y = bezierPoint(bezPts[0][1], bezPts[1][1], bezPts[2][1], bezPts[3][1], t);
    //ellipse(x, y, 5, 5);
    vertices.add(new PVector(x,y));
    heart.add(new PVector(x,y));
  } 

  for (int i=steps-1; i>0;i--){
     PVector pt = vertices.get(i);
     //ellipse(-1*pt.x, pt.y, 5, 5);
     vertices.add(new PVector(width/2 -(pt.x-width/2),pt.y));
     heart.add(new PVector(width/2 -(pt.x-width/2),pt.y));
  }
}
void makeBigHeart(){
   noFill();
   float[][] bezPts = new float[4][2];
   bezPts[0][0] = width/2;
   bezPts[0][1] = height/2-height/5;

   bezPts[1][0] = width/2+3*width/12;
   bezPts[1][1] = height/2-7*height/16;

   bezPts[2][0] = width/2+width/3;
   bezPts[2][1] = height/2+height/16;

   bezPts[3][0] = width/2;
   bezPts[3][1] = height/2+height/4;

   float scale = 1.55;

   for(int i=0;i<4;i++){

     float newX = width/2+(bezPts[i][0]-width/2)*scale;
      float newY = height/2+5+(bezPts[i][1]-height/2)*scale; 
      bezPts[i][0] = newX;
      bezPts[i][1] = newY;
   }

  //bezier(bezPts[0][0],bezPts[0][1],bezPts[1][0],bezPts[1][1],bezPts[2][0],bezPts[2][1],bezPts[3][0],bezPts[3][1]);
  fill(255);
  int steps = 10+int(scale);
  //vertices = new ArrayList<PVector>();
  bigheart = new ArrayList<PVector>();
  for (int i = 0; i <= steps; i++) {
    float t = i / float(steps);
    float x = bezierPoint(bezPts[0][0], bezPts[1][0], bezPts[2][0], bezPts[3][0], t);
    float y = bezierPoint(bezPts[0][1], bezPts[1][1], bezPts[2][1], bezPts[3][1], t);
    //ellipse(x, y, 5, 5);

    vertices.add(new PVector(x,y));
    bigheart.add(new PVector(x,y));

    if (i >0 && i<steps){
     vertices.add(new PVector(width/2 -(x-width/2),y));
     bigheart.add(new PVector(width/2 -(x-width/2),y)); 
    }

  } 

}
void makeBigHeart2(){
   noFill();
   float[][] bezPts = new float[4][2];
   bezPts[0][0] = width/2;
   bezPts[0][1] = height/2-height/5;

   bezPts[1][0] = width/2+3*width/12;
   bezPts[1][1] = height/2-7*height/16;

   bezPts[2][0] = width/2+width/3;
   bezPts[2][1] = height/2+height/16;

   bezPts[3][0] = width/2;
   bezPts[3][1] = height/2+height/4;

   float scale = 2.3;

   for(int i=0;i<4;i++){

     float newX = width/2+(bezPts[i][0]-width/2)*scale;
      float newY = height/2+(bezPts[i][1]-height/2)*scale; 
      bezPts[i][0] = newX;
      bezPts[i][1] = newY;
   }

  //bezier(bezPts[0][0],bezPts[0][1],bezPts[1][0],bezPts[1][1],bezPts[2][0],bezPts[2][1],bezPts[3][0],bezPts[3][1]);
  fill(255);
  int steps = 10+int(scale);
  //vertices = new ArrayList<PVector>();
  //bigheart2 = new ArrayList<PVector>();
  for (int i = 0; i <= steps; i++) {
    float t = i / float(steps);
    float x = bezierPoint(bezPts[0][0], bezPts[1][0], bezPts[2][0], bezPts[3][0], t);
    float y = bezierPoint(bezPts[0][1], bezPts[1][1], bezPts[2][1], bezPts[3][1], t);
    //ellipse(x, y, 5, 5);
    PVector pos = new PVector(x,y);
    if(isInsidePolygon(pos,box)){
      vertices.add(new PVector(x,y));
      //bigheart2.add(new PVector(x,y));

      if (i >0 && i<steps){
       vertices.add(new PVector(width/2 -(x-width/2),y));
       //bigheart2.add(new PVector(width/2 -(x-width/2),y)); 
      }
    }

  } 

}
void addBox(){
   int idealspacing = 50;
   float border = 72.0/4;
   int xPts = round((width-border*2)/idealspacing);
   int yPts = round((height-border*2)/idealspacing);

   float xSpacing = (width-border*2)/(xPts-1);
   float ySpacing = (height-border*2)/(yPts-1);
   box = new ArrayList<PVector>();
   //add corners
   vertices.add(new PVector(border,border));
   vertices.add(new PVector(width-border,border));
   vertices.add(new PVector(border,height-border));
   vertices.add(new PVector(width-border,height-border));

   box.add(new PVector(border,border));
   box.add(new PVector(width-border,border));
   box.add(new PVector(width-border,height-border));
   box.add(new PVector(border,height-border));

   //add top and bottom borders
   for(int i=1;i<xPts-1;i++){
       vertices.add(new PVector(border+i*xSpacing,border));
       vertices.add(new PVector(border+i*xSpacing,height-border));
   }
   //add left and right borders
   for(int i=1;i<yPts-1;i++){
       vertices.add(new PVector(border, border+i*ySpacing));
       vertices.add(new PVector(width-border,border+i*ySpacing));
   }

   /*
   for(int i=1;i<xPts-1;i++){
      for(int j=1;j<yPts-1;j++){
         PVector pos = new PVector(border+i*xSpacing+jitter(10.0),border+j*ySpacing+jitter(10.0));
         if(!isInsidePolygon(pos,bigheart)){
             vertices.add(pos);
         }
      } 
   }*/

}

float jitter(float inp){
   float num = random(-inp,inp);
  return num; 
}

void voronoiInit(){
  voronoi = new Voronoi(DSIZE);
  int size = vertices.size();
  for (int i=0;i<size;i++){
      PVector pt = vertices.get(i);
      voronoi.addPoint(new Vec2D(pt.x,pt.y));
  }
}

void drawVerts(){
    int size = vertices.size();
  for (int i=0;i<size;i++){
      PVector pt = vertices.get(i);
      stroke(0);
      fill(255);
      ellipse(pt.x,pt.y,5,5);
  }
}

void setup (){
  size(494,360,P3D);
  smooth();

  makeHeart();
  makeBigHeart();

  addBox();
  makeBigHeart2();
  voronoiInit();
}

void draw (){
  if (record) {
    beginRaw(DXF, "output.dxf");
  }
  background(255);
  fill(200,0,0);
  float border = 72.0/4;
  rect(border,border,width-2*border,height-2*border);
  for (Triangle2D t : voronoi.getTriangles()) {
    t.computeCentroid();
    PVector pos = new PVector(t.centroid.x,t.centroid.y);
    if(isInsidePolygon(pos,box)){
      if(!isInsidePolygon(pos,heart)){
        beginShape(TRIANGLES);
        noFill();
        stroke(255);
        strokeJoin(ROUND);
        strokeWeight(5);
        vertex(t.a.x, t.a.y);
        vertex(t.b.x, t.b.y);
        vertex(t.c.x, t.c.y);
        endShape();
      }
    }
  }
  fill(255);
  noStroke();
  beginShape();
  int size = heart.size();
  for(int i=0;i<size;i++){
    PVector pt = heart.get(i);
    vertex(pt.x,pt.y);
  }
  endShape(CLOSE);
  //drawVerts();
  if (record) {
    endRaw();
    record = false;
  }
}

public boolean isInsidePolygon(PVector pos,ArrayList<PVector> shape) {
    int i, j=shape.size()-1;
    int sides = shape.size();
    boolean oddNodes = false;
    int count=0;

    for (i=0; i<sides; i++) {
        PVector pt1 = shape.get(i);
        PVector pt2 = shape.get(j);
        if (((pt2.y > pos.y) != (pt1.y > pos.y)) && (pos.x < (pt1.x - pt2.x) * (pos.y - pt2.y) / (pt1.y - pt2.y) + pt2.x)){
            oddNodes= !oddNodes;
        }
        j=i; 
    }
    return oddNodes;
}

void keyPressed() {
  // Use a key press so that it doesn't make a million files
  if (key == 'r') {
    record = true;
  }
}

From here, I was able to put in a quick function that saves the output as a .DXF file.  I had to bring it into Solidworks, clean up the lines, and scale it, and then do some further modification in Illustrator before saving it as a .SVG.  From there I used MakerCam to generate my GCode.  It is a pretty nice little online app written in Flash that allows you to do generate toolpathing for 2.5D operations.  Here are some pictures of the milling process:

  • Blank linoleum mounted to an MDF block, and blank cards.

The experiment worked, for the most part.  When cutting the thin lines with a two-flute 1/16″ endmill, the linoleum chips just tended to gum up and stay in the groove.  I think I might be able to fix some of that by slowing down the cutter RPMs and feedrate, but that will take some experimentation later down the road.  Cutting with the 1/8″ endmill went smoothly.  After the block cutting was done, I had to spend some time clearing the grooves of cuttings.  After trying an exacto, I found that a tiny allen key was the perfect tool to clear the groove. Next came the printing:

  • Printing materials.

As you can probably see, I am by no means experienced in lino-block printing.  I also bought the cheapest brayer & inks I could find, so this is probably the reason my prints only came out so-so.  But, overall, I think it was a success.  Personally, I think that a bit of smudging and inconsistent ink layer is part of the charm of lino-block prints, and it contrasts nicely with geometric design.

As another little side project, I came up with the idea for a spiral-sweep heart-shaped pen holder for my girlfriend… because I’m pretty sure that is what she has always wanted.  One of the interesting things about the 3D printing, and using GCode generator, Slic3r, is that inputting the settings for a specific print is really an artform.  Because you can specify different layer heights, wall thicknesses, and other parameters, all I had to do was create the swept solid in solidworks, and export that as an STL.

  • Spiral sweep in Solidworks

Once in slicer, I set it to print without a top surface or infill, and to create a two layer wall thickness.  Afterwards, I checked the GCode with the GCode Analyzer.  It gives you a time estimate, as well as 2D and 3D views of the toolpaths.  The result was a quick (~1hr) print on the Mendel Prusa at NextFab!

  • Print beginning on the Mendel Prusa.

All-in-all, a really productive making session.  Learned some new tricks in Processing.  Cut some new material on my Shapeoko.  And figured out a few new things with 3D printing.  Open-source is awesome!

Leave a Reply

Your email address will not be published. Required fields are marked *