Following on from my initial playing around with Tinkercad and its Shape Script implementation, I started to look more closely into what’s possible with the Gen6 kernel.
I came across this recent blog post, which highlights the ability to make Shape Script implementation details public. The Shape Scripts shown in the post looked nice and complex, so I tracked down the model by selecting “Discover” inside Tinkercad and searching for “Brandon Cole”, the originator of these very interesting Shape Scripts. He published this design about 3 weeks ago, which I went ahead and copied (yes, shamelessly, but at least I’m admitting to it :-) via the “Copy & Tinker” button.
Inside the model, Brandon’s Shape Scripts were visible in the right-sash. Selecting and editing them – as he’d kindly made them public – brought up their underlying code. Brandon used a similar approach for each of them (with some minor implementation differences), the primary difference being the mathematical equations that represent the various shapes he chose to display.
I went looking for additional, interesting mathematical shapes to bring into Brandon’s framework. And so it was that I discovered knot theory, which is the mathematical study of knots. These are ripe for representation in Tinkercad, as mathematical knots seem generally to be based on the idea of a closed loop being knotted in some way, and – as we found, last time – the exposure of the Gen6 kernel to Shape Scripts currently requires a single mesh or solid to be created (although it is possible to extrude a set of disjoint paths, as we also saw last time).
Brandon has cleverly worked around the limitation of not being able to extrude along a 3D path by meshing the extrusion manually. Very cool stuff.
Anyway, back to knots. It turns out that – beyond the Trefoil and Torus that Brandon has already modeled – there are all kinds of mathematical knots with names such as Cinquefoil, Septafoil and Stevedore. I was looking for knots with a published parametric representation – as I want both the equation for the knot’s X, Y, Z coordinates and the equation’s derivative – so I ended up choosing the Figure 8 knot.
Plugging the knot’s formulae – for each of the X, Y and Z values – into the code was straightforward, but I needed a little help finding their respective derivatives. I ended up using this online calculator (what I wouldn’t have given for one of these, 20+ years ago!), which saved me from the mental contortions I’d have needed to determine them, otherwise.
Here’s the resultant Shape Script code, courtesy of Brandon Cole, with my own edits on the lines marked “Equation” and “Equation Derivative” (in addition to the simple transformation at the end).
params = [
{
"id": "size",
"displayName": "Size",
"type": "float",
"rangeMin": 1,
"rangeMax": 50,
"default": 40
},
{
"id": "radius",
"displayName": "Radius",
"type": "float",
"rangeMin": 1,
"rangeMax": 10,
"default": 1
}
];
/**
* @class
*/
XYZ = function (x, y, z) {
this.x = x;
this.y = y;
this.z = z;
};
XYZ.prototype = {
add: function (other) {
this.x += other.x;
this.y += other.y;
this.z += other.z;
return this;
},
sub: function (other) {
this.x -= other.x;
this.y -= other.y;
this.z -= other.z;
return this;
},
scale: function (scale) {
this.x *= scale;
this.y *= scale;
this.z *= scale;
return this;
},
length: function () {
return Math.sqrt((this.x * this.x) +
(this.y * this.y) +
(this.z * this.z));
},
normalize: function () {
this.scale(1 / this.length());
return this;
},
cross: function (other) {
return new XYZ((this.y * other.z) - (this.z * other.y),
(this.z * other.x) - (this.x * other.z),
(this.x * other.y) - (this.y * other.x));
},
dot: function (other) {
return (this.x * other.x) +
(this.y * other.y) +
(this.z * other.z);
},
clone: function () {
return new XYZ(this.x, this.y, this.z);
},
transformByQuat: function (quat) {
var vq = new Quaternion(this.x, this.y, this.z, 0.0);
var v1 = quat.congugate().transformByQuat(vq);
var v2 = quat.transformByQuat(v1);
var v2 = v1.transformByQuat(quat);
return new XYZ(v2.x, v2.y, v2.z);
},
toArray: function () {
return [this.x, this.y, this.z];
}
};
/**
* @class
*/
Quaternion = function (x, y, z, w) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
};
Quaternion.prototype = {
congugate: function () {
return new Quaternion(-this.x, -this.y, -this.z, this.w);
},
fromAxisAngle: function (axis, angle) {
var halfAngle = angle * 0.5;
var sinAngle = Math.sin(halfAngle);
this.x = (axis.x * sinAngle);
this.y = (axis.y * sinAngle);
this.z = (axis.z * sinAngle);
this.w = Math.cos(halfAngle);
return this;
},
transformByQuat: function (quat) {
return new Quaternion(
this.w * quat.x + this.x * quat.w +
this.y * quat.z - this.z * quat.y,
this.w * quat.y + this.y * quat.w +
this.z * quat.x - this.x * quat.z,
this.w * quat.z + this.z * quat.w +
this.x * quat.y - this.y * quat.x,
this.w * quat.w - this.x * quat.x -
this.y * quat.y - this.z * quat.z);
},
normalize: function () {
}
};
function nearest(xyz, list) {
var result = 0;
var distance = xyz.clone().sub(list[0]).length();
for (var i = 1; i < list.length; i++) {
var len = xyz.clone().sub(list[i]).length();
if (len < distance) {
distance = len;
Debug.log(distance)
result = i;
}
}
return result;
};
/**
* @function
*/
function tesselate(mesh, points, derivs, normals, radius) {
var ndivs = Tess.circleDivisions(radius);
var t = 0;
var circles = [];
var len = points.length;
for (t = 0; t < len; t++) {
// Build the geometry...
var circle = [];
for (var j = 0; j < ndivs; j++) {
var alpha = (Math.PI * 2 * (j / ndivs));
var q = new Quaternion().fromAxisAngle(derivs[t], alpha);
var n = normals[t].clone().transformByQuat(q);
var p = points[t].clone().add(n.scale(radius));
circle.push(p);
}
circle.push(circle[0]);
circles.push(circle);
}
// Tesselate the geometry...
for (t = 0; t < circles.length - 1; t++) {
var start = circles[t];
var end = circles[t + 1];
var ndx = nearest(start[0], end);
Debug.log(ndx);
for (var u = 0; u < start.length - 1; u++) {
var pt1 = start[u];
var pt2 = start[u + 1];
var pt3 = end[(ndx + u) % (end.length - 1)];
var pt4 = end[(ndx + u + 1) % (end.length - 1)];
mesh.triangle(pt1.toArray(), pt2.toArray(), pt3.toArray());
mesh.triangle(pt2.toArray(), pt4.toArray(), pt3.toArray());
}
}
};
function process(params) {
var size = params.size;
var radius = params.radius;
var ndivs = Tess.circleDivisions(20) * 3;
var mesh = new Mesh3D();
var i;
var t;
var pt;
var pt_d;
var pt_n;
var points = []; // Points positions
var derivs = []; // Point derivatives
var normals = []; // Point normals
// Calculate the points and derivatives...
for (i = 0; i < ndivs; i++) {
// Calculate (t)...
t = (i / ndivs) * (Math.PI * 2);
// Equation...
pt = new XYZ((2 + Math.cos(2 * t)) * Math.cos(3 * t),
(2 + Math.cos(2 * t)) * Math.sin(3 * t),
Math.sin(4 * t));
// Equation Derivative...
pt_d = new XYZ(-3 * (Math.cos(2 * t) + 2) * Math.sin(3 * t) -
2 * Math.sin(2 * t) * Math.cos(3 * t),
3 * (Math.cos(2 * t) + 2) * Math.cos(3 * t) -
2 * Math.sin(2 * t) * Math.sin(3 * t),
4 * Math.cos(4 * t));
// Scale Equation...
pt.scale(size / 4);
// Normalize Derivative...
pt_d.normalize();
// Store...
points.push(pt);
derivs.push(pt_d);
}
// Calculate the normals...
for (i = 0; i < ndivs; i++) {
var pt1 = points[i];
var pt2 = pt1.clone().add(derivs[i]);
var pt3 = ((ndivs - 1) === i) ? points[0] : points[i + 1];
var ux = pt2.clone().sub(pt1).normalize();
var uv = pt3.clone().sub(pt1).normalize();
var norm = ux.cross(uv).normalize();
normals.push(norm);
}
points.push(points[0]);
derivs.push(derivs[0]);
normals.push(normals[0]);
// Tesselate...
tesselate(mesh, points, derivs, normals, radius);
// Make the solid...
var solid = Solid.make(mesh);
// Rotate 90 degrees around the Z-axis for visibility's sake
solid.transform(
[
0, -1, 0, 0,
1, 0, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
);
return solid;
};
And here’s the “tinkered” model with an instance of my Shape Script in it (with the other objects moved around to make some space in the workplane):