Programming a spiral

As I wrote in my previous post, I decided to program a Custom Shape Generator in Tinkercad in order to be able to generate every detail of my bowl feeder just as I want it to be. I spent my two weeks summer vacation in Italy thinking about sine and cosine, vectors and loops. I implemented the JavaScript code on my cellphone, although characters such as the square brackets [] were a challenge and I needed to install a special Android keyboard app for these. When coming back from Italy it was almost finished, only a few minor details needed to be fixed.

Before I describe the implementation and the challenges in detail, I’d like to show a screenshot from Tinkercad how the result looks like:

screenshot of bowl feeder in Tinkercad
bowl in Tinkercad generated by my custom shape generator

The implementation mainly consists of a big loop that fills an array of points. After that, a second loop builds triangles from these points and makes a so-called mesh.

Similar to slicing a cake into pieces, the loop calculates one slice (“segment”) after the other in a counter-clockwise direction.

To calculate the coordinates of one point (or “vertex”) I first calculate delta-r and delta-h, which are measured from the beginning of the ramp. These are then tilted outwards (if desired by the user), so that pieces better lie on the ramp and can lean on the outer wall. Finally, r and h are rotated for the current segment to obtain the triplet [x, y, z].

function shapeGeneratorEvaluate(params, callback) {
  var tilt = params["outtilt"] / 360 * 2*Math.PI;
  var verts = [];

  // for all segments:
  for (s = 0; s < params["numsegments"]; s++) {
    // generate array of points for one segment
    var seg = [];

    // first bottom point
    dr = s / params["numsegments"] * params["rampwidth"];
    dh = 0;
    r = params["bottomradius"] + Math.cos(tilt) * dr + Math.sin(tilt) * dh;
    h = Math.cos(tilt) * dh - Math.sin(tilt) * dr;
    x = r * Math.cos(2*Math.PI * s / params["numsegments"]);
    y = r * Math.sin(2*Math.PI * s / params["numsegments"]);
    z = h;
    seg.push([x, y, z]);

    /* ... */

    verts.push(seg);
  }

After the first bottom point, a loop calculates the points for each winding. Since we are generating a spiral, the radius increases with each segment and each winding.

When all points of the verts array have been calculated, triangles/quads need to be defined from them. This is done in a nested loop for all segments and all windings. Some special care needs to be taken when connecting the points from the last segment with the first segment in order to connect the correct points. This is also when the “exit hole” must correctly be dealt with.

Following is the complete program as I use it at the moment to generate my bowl feeder models:

var Conversions = Core.Conversions;
var Debug = Core.Debug;
var Path2D = Core.Path2D;
var Point2D = Core.Point2D;
var Point3D = Core.Point3D;
var Matrix2D = Core.Matrix2D;
var Matrix3D = Core.Matrix3D;
var Mesh3D = Core.Mesh3D;
var Plugin = Core.Plugin;
var Tess = Core.Tess;
var Sketch2D = Core.Sketch2D;
var Solid = Core.Solid;
var Vector2D = Core.Vector2D;
var Vector3D = Core.Vector3D;

params = [
  {
    "id": "bottomradius",
    "displayName": "Bottom Inner Radius",
    "type": "length",
    "rangeMin": 1.0,
    "rangeMax": 50.0,
    "default": 10.0
  },
  {
    "id": "numwindings",
    "displayName": "Number of Windings",
    "type": "int",
    "rangeMin": 1,
    "rangeMax": 50,
    "default": 2
  },
  {
    "id": "windingheight",
    "displayName": "Height of a Winding",
    "type": "length",
    "rangeMin": 1.0,
    "rangeMax": 50.0,
    "default": 2.0
  },
  {
    "id": "rampwidth",
    "displayName": "Ramp Width",
    "type": "length",
    "rangeMin": 1.0,
    "rangeMax": 50.0,
    "default": 1.0
  },
  {
    "id": "outtilt",
    "displayName": "Outwards Tilt",
    "type": "angle",
    "rangeMin": 0,
    "rangeMax": 45,
    "default": 10
  },
  {
    "id": "numsegments",
    "displayName": "Number of Segments",
    "type": "int",
    "rangeMin": 5,
    "rangeMax": 360,
    "default": 48
  },
  {
    "id": "minwallthickness",
    "displayName": "Minimal Wall Thickness",
    "type": "length",
    "rangeMin": 1.0,
    "rangeMax": 20.0,
    "default": 1.0
  },
  {
    "id": "exitlength",
    "displayName": "Exit Length",
    "type": "length",
    "rangeMin": 1.0,
    "rangeMax": 50.0,
    "default": 10.0
  }
];


function shapeGeneratorEvaluate(params, callback) {
  var tilt = params["outtilt"] / 360 * 2*Math.PI;

  // generate array of all points of the mesh
  var verts = [];

  var centerbottom, centertop;  // z coordinate
  var bowlheight;
  var exithole = [];  // points of ramp exit
  
  // loop variables
  var s, w;
  
  var dr, dh, r, h;  // radius and height (temp.)
  var x, y, z;

  centertop = Math.tan(tilt) * params["bottomradius"];
  centerbottom = 0 - Math.sin(tilt) * params["rampwidth"] - params["minwallthickness"];
  
  dr = params["numwindings"] * params["rampwidth"];
  dh = (params["numwindings"] + 1) * params["windingheight"];
  bowlheight = Math.cos(tilt) * dh - Math.sin(tilt) * dr;
    
  // for all segments:
  for (s = 0; s < params["numsegments"]; s++) {
    // generate array of points for one segment
    var seg = [];

    // first bottom point
    dr = s / params["numsegments"] * params["rampwidth"];
    dh = 0;
    r = params["bottomradius"] + Math.cos(tilt) * dr + Math.sin(tilt) * dh;
    h = Math.cos(tilt) * dh - Math.sin(tilt) * dr;
    x = r * Math.cos(2*Math.PI * s / params["numsegments"]);
    y = r * Math.sin(2*Math.PI * s / params["numsegments"]);
    z = h;
    seg.push([x, y, z]);

    for (w = 0; w < params["numwindings"]; w++) {
      // inner point of the ramp
      dr = (s / params["numsegments"] + w) * params["rampwidth"];
      dh = (s / params["numsegments"] + w) * params["windingheight"];
      r = params["bottomradius"] + Math.cos(tilt) * dr + Math.sin(tilt) * dh;
      h = Math.cos(tilt) * dh - Math.sin(tilt) * dr;
      x = r * Math.cos(2*Math.PI * s / params["numsegments"]);
      y = r * Math.sin(2*Math.PI * s / params["numsegments"]);
      z = h;

      seg.push([x, y, z]);

      // outer point of the ramp
      dr = (s / params["numsegments"] + w + 1) * params["rampwidth"];
      dh = (s / params["numsegments"] + w) * params["windingheight"];
      r = params["bottomradius"] + Math.cos(tilt) * dr + Math.sin(tilt) * dh;
      h = Math.cos(tilt) * dh - Math.sin(tilt) * dr;
      x = r * Math.cos(2*Math.PI * s / params["numsegments"]);
      y = r * Math.sin(2*Math.PI * s / params["numsegments"]);
      z = h;

      seg.push([x, y, z]);
    }
    
    // top rim
    dr = (s / params["numsegments"] + params["numwindings"]) * params["rampwidth"];
    dh = (params["numwindings"] + 1) * params["windingheight"];
    r = params["bottomradius"] + Math.cos(tilt) * dr + Math.sin(tilt) * dh;
    h = Math.cos(tilt) * dh - Math.sin(tilt) * dr;
    // after tilt, extend to full height
    r += (bowlheight - h) * Math.tan(tilt);
    x = r * Math.cos(2*Math.PI * s / params["numsegments"]);
    y = r * Math.sin(2*Math.PI * s / params["numsegments"]);
    z = bowlheight;

    seg.push([x, y, z]);

    // outer top rim (circle)
    dr = (params["numwindings"] + 1) * params["rampwidth"] + params["minwallthickness"]; 
    dh = (params["numwindings"] + 1) * params["windingheight"];
    r = params["bottomradius"] + Math.cos(tilt) * dr + Math.sin(tilt) * dh;
    h = Math.cos(tilt) * dh - Math.sin(tilt) * dr;
    // after tilt, extend to full height
    r += (bowlheight - h) * Math.tan(tilt);
    x = r * Math.cos(2*Math.PI * s / params["numsegments"]);
    y = r * Math.sin(2*Math.PI * s / params["numsegments"]);
    z = bowlheight;

    seg.push([x, y, z]);

    // outer middle ring (circle)
    dr = (params["numwindings"] + 1) * params["rampwidth"] + params["minwallthickness"]; 
    dh = params["numwindings"] * params["windingheight"] - params["minwallthickness"];
    r = params["bottomradius"] + Math.cos(tilt) * dr + Math.sin(tilt) * dh;
    h = Math.cos(tilt) * dh - Math.sin(tilt) * dr;
    x = r * Math.cos(2*Math.PI * s / params["numsegments"]);
    y = r * Math.sin(2*Math.PI * s / params["numsegments"]);
    z = h;

    seg.push([x, y, z]);

    // bottom rim (circle)
    dr = params["rampwidth"];
    dh = 0.0;
    r = params["bottomradius"] + Math.cos(tilt) * dr + Math.sin(tilt) * dh + params["minwallthickness"];
    h = Math.cos(tilt) * dh - Math.sin(tilt) * dr - params["minwallthickness"];
    x = r * Math.cos(2*Math.PI * s / params["numsegments"]);
    y = r * Math.sin(2*Math.PI * s / params["numsegments"]);
    z = h;

    seg.push([x, y, z]);

    verts.push(seg);
  }

  // calculate ramp exit points
  // lower inner point
  dr = params["numwindings"] * params["rampwidth"];
  dh = params["numwindings"] * params["windingheight"];
  r = params["bottomradius"] + Math.cos(tilt) * dr + Math.sin(tilt) * dh;
  h = Math.cos(tilt) * dh - Math.sin(tilt) * dr;
  exithole.push([r, 0, h]);
  
  // lower outer point
  dr = (params["numwindings"] + 1) * params["rampwidth"];
  dh = params["numwindings"] * params["windingheight"];
  r = params["bottomradius"] + Math.cos(tilt) * dr + Math.sin(tilt) * dh;
  h = Math.cos(tilt) * dh - Math.sin(tilt) * dr;
  exithole.push([r, 0, h]);
  
  // higher outer point
  dr = (params["numwindings"] + 1) * params["rampwidth"];
  dh = (params["numwindings"] + 1) * params["windingheight"];
  r = params["bottomradius"] + Math.cos(tilt) * dr + Math.sin(tilt) * dh;
  h = Math.cos(tilt) * dh - Math.sin(tilt) * dr;
  // after tilt, extend to full height
  r += (bowlheight - h) * Math.tan(tilt);
  exithole.push([r, 0, bowlheight]);
  
  // higher inner point
  exithole.push(verts[0][params["numwindings"] * 2 + 1]);
  

  // create mesh from array
  var mesh = new Mesh3D();

  for (s = 0; s < params["numsegments"]; s++) {
    var nexts = (s+1 === params["numsegments"]) ? 0 : (s+1);
 
    mesh.triangle([0, 0, centertop], verts[s][0], verts[nexts][0]);
    
    for (w = 0; w < verts[s].length - 1; w++) {
      if (nexts !== 0) {
        // not the last segment
        mesh.quad(verts[s][w], verts[s][w+1], verts[nexts][w+1], verts[nexts][w]);
      } else {
        // last segment
        if (w < params["numwindings"] * 2 - 2) {
          // each winding connects to the next
          mesh.quad(verts[s][w], verts[s][w+1], verts[0][w+3], verts[0][w+2]);
        } else if (w === params["numwindings"] * 2 - 2) {
          // wall below exithole
          mesh.quad(verts[s][w], verts[s][w+1], exithole[0], verts[0][w+2]);
        } else if (w === params["numwindings"] * 2 - 1) {
          // top winding connects to exit ramp
          mesh.quad(verts[s][w], verts[s][w+1], exithole[1], exithole[0]);
        } else if (w === params["numwindings"] * 2) {
          // wall of top winding
          mesh.quad(verts[s][w], verts[s][w+1], exithole[2], exithole[1]); 
        } else if (w === params["numwindings"] * 2 + 1) {
          // top rim
          mesh.quad(verts[s][w], verts[s][w+1], verts[0][w+1], exithole[2]);
        } else {
          // outside
          mesh.quad(verts[s][w], verts[s][w+1], verts[0][w+1], verts[0][w]);
        }
        
      }
    }
    
    mesh.triangle([0, 0, centerbottom], verts[nexts][verts[nexts].length-1], verts[s][verts[nexts].length-1]);
  }
  
  // close exit hole to make mesh watertight 
  mesh.quad(exithole[0], exithole[1], exithole[2], exithole[3]);
  
  callback(Solid.make(mesh)); 
  return;
}

PS: It is worth noting that the first versions of my program also tried to make a cut through the outer wall and attach an exit ramp so that pieces can exit the bowl. This was implemented using the methods Mesh3D.unite() and Mesh3D.subtract(). However, I could not get it to work properly. Either these functions are buggy or my mesh had some invisible errors (e.g. triangles facing inwards or holes in the surface). Anyway, the result was that lots of unneeded vertices were added and the mesh slightly deformed. This caused problems when opening it in other software.

early version of the bowl with exit ramp
a version of the bowl with attached exit ramp

After I couldn’t solve this despite putting in much work, I finally decided to attach the exit ramp at a later stage, not in the custom shape generator. More on that in my next post!

Leave a Reply

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

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.