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:
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.
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!