Skip to content

Instantly share code, notes, and snippets.

@theGOTOguy
Created April 6, 2023 06:35
Show Gist options
  • Save theGOTOguy/dca3d0e9fc7c8446a8951234d35633b5 to your computer and use it in GitHub Desktop.
Save theGOTOguy/dca3d0e9fc7c8446a8951234d35633b5 to your computer and use it in GitHub Desktop.
Bezier Bean Source Code for TinkerCAD "Cloud Gate"
// Convenience Declarations For Dependencies.
// 'Core' Is Configured In Libraries Section.
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 = [
// Basic parameters.
{ "id": "width", "displayName": "Width", "type": "length", "rangeMin": 1.0, "rangeMax": 200.0, "default": 6.5 },
{ "id": "length", "displayName": "Length", "type": "length", "rangeMin": 1.0, "rangeMax": 200.0, "default": 10.0 },
{ "id": "height", "displayName": "Height", "type": "length", "rangeMin": 1.0, "rangeMax": 200.0, "default": 5.0 },
{ "id": "ellipse", "displayName": "Ellipse", "type": "float", "rangeMin": 1.0, "rangeMax": 3.0, "default": 2.05 },
// Parameters for the omphalos control point.
{ "id": "oheight", "displayName": "Omphalos height", "type": "length", "rangeMin": 1.0, "rangeMax": 100.0, "default": 4.1 },
{ "id": "oradius", "displayName": "Omphalos radius", "type": "length", "rangeMin": 1.0, "rangeMax": 50.0, "default": 1.3 },
{ "id": "opinch", "displayName": "Omphalos pinch", "type": "length", "rangeMin": 1.0, "rangeMax": 50.0, "default": 0.5 },
// Parameters for lower control point.
{ "id": "archsoftness", "displayName": "Arch Softness", "type": "float", "rangeMin": 0.0, "rangeMax": 0.5, "default": 0.35},
{ "id": "archheight", "displayName": "Arch height", "type": "length", "rangeMin": 1.0, "rangeMax": 100.0, "default": 1.4 },
{ "id": "archradius", "displayName": "Proportion of radius at arch", "type": "float", "rangeMin": 0.01, "rangeMax": 1, "default": 0.7},
// Parameters for outermost control point.
{ "id": "lheight", "displayName": "Height at length", "type": "length", "rangeMin": 1.0, "rangeMax": 200.0, "default": 1.5},
{ "id": "wheight", "displayName": "Height at width", "type": "length", "rangeMin": 1.0, "rangeMax": 200.0, "default": 2.5},
// Round
{ "id": "lfullness", "displayName": "Lengthwise Fullness", "type": "float", "rangeMin": 0.01, "rangeMax": 1, "default": 0.54},
{ "id": "wfullness", "displayName": "Widthwise Fullness", "type": "float", "rangeMin": 0.01, "rangeMax": 1, "default": 0.58},
{ "id": "ltoproundness", "displayName": "Lengthwise Roundness", "type": "float", "rangeMin": 0.01, "rangeMax": 1, "default": 0.54},
{ "id": "wtoproundness", "displayName": "Widthwise Roundness", "type": "float", "rangeMin": 0.01, "rangeMax": 1, "default": 0.53},
{ "id": "lowroundness", "displayName": "Underside Roundness", "type": "float", "rangeMin": 0.01, "rangeMax": 1, "default": 0.54},
// Resolution.
{ "id": "resolution", "displayName": "Resolution", "type": "int", "rangeMin": 4, "rangeMax": 20, "default": 60}
];
function binom(n, k) {
var coeff = 1;
for (var i = n-k+1; i <= n; i++) coeff *= i;
for (var i = 1; i <= k; i++) coeff /= i;
return coeff;
}
function bezier(points, controlPoints, include_first, include_last, resolution) {
var first = include_first ? 1 : 0;
var last = include_last ? resolution : resolution - 1;
for (var i = first; i <= last; i++) {
var thisPoint;
var t;
thisPoint = [0, 0];
t = i / resolution;
for (var j = 0; j < controlPoints.length; j++) {
var coefficient;
coefficient = Math.pow(1 - t, controlPoints.length - j - 1) *
Math.pow(t, j) *
binom(controlPoints.length - 1, j);
thisPoint[0] += coefficient * controlPoints[j][0];
thisPoint[1] += coefficient * controlPoints[j][1];
}
points.push(thisPoint);
}
return points;
}
function get2DPointsForSlice(height, length, baseheight, baseradius, rheight, roofheight, oradius, oheight, opinch, toproundness, lowroundness, fullness, resolution) {
var points = [];
// Top to outer
bezier(points, [[0, height],
[length * fullness, height],
[length, rheight + (height - rheight) * toproundness],
[length, rheight]],
false, true, resolution);
// Outer to bottom
bezier(points, [[length, rheight],
[length, baseheight + (rheight - baseheight) * (1 - lowroundness)],
[baseradius + (length - baseradius) * lowroundness, baseheight],
[baseradius, baseheight]],
false, true, resolution);
// Bottom to inside of omphalos
bezier(points, [[baseradius, baseheight],
[oradius - opinch, baseheight], // This is just a guess. It may need further tweaking.
[oradius, oheight],
[0, oheight]],
false, false, resolution);
/*
Debug.log(points);
Debug.log(points[0]);
*/
return points;
}
// This could be improved with a binary search, if necessary.
function lookup_x(curvepoints, x, max_i) {
for (var i = 0; i < max_i; i++) {
if (curvepoints[i][0] >= x)
return curvepoints[i][1];
}
return curvepoints[0][max_i - 1];
}
// This function simply gets the points in 2D, and then puts them on the right plane.
function get3DPointsForSlice(params, angle) {
var slicelength = params['length'] / 2; // Remember, each slice is only half the bean (because we are in polar coordinates)
var slicewidth = params['width'] / 2;
var e = params['ellipse'];
var r = slicelength * slicewidth / (
Math.pow(Math.pow(Math.abs(slicelength * Math.cos(angle)), e) +
Math.pow(Math.abs(slicewidth * Math.sin(angle)), e),
1 / e));
// Base height for this slice.
// I'm going to use the Bezier function also to get the base height, since I want it to be "flat" for a while.
var archpoints = [];
var archresolution = params['resolution'] * 8; // Resolution * 4 is an arbitrary choice. Actual resolution used should be at least as big as resolution.
bezier(archpoints, [[0, params['archheight']],
[(Math.PI / 2) * params['archsoftness'], params['archheight']],
[(Math.PI / 2) * (1 - params['archsoftness']), 0],
[Math.PI / 2, 0]],
true, true, archresolution);
var baseheight;
if (Math.PI / 2 >= angle) {
baseheight = lookup_x(archpoints, angle, archresolution);
} else if (Math.PI >= angle) {
baseheight = lookup_x(archpoints, Math.PI - angle, archresolution);
} else if (3 * Math.PI / 2 >= angle) {
baseheight = lookup_x(archpoints, angle - Math.PI, archresolution);
} else {
baseheight = lookup_x(archpoints, 2 * Math.PI - angle, archresolution);
}
//var baseheight = params['archheight'] * Math.pow(Math.cos(angle), 2);
// Base radius for this slice.
var baseradius = r * params['archradius'];
var rheight = Math.sqrt(Math.pow(params['lheight'] * Math.sin(angle), 2) + Math.pow(params['wheight'] * Math.cos(angle), 2));
var fullness = Math.pow(Math.pow(Math.abs(params['lfullness'] * Math.sin(angle)), 2) + Math.pow(Math.abs(params['wfullness'] * Math.cos(angle)), 2),
1 / 2);
var toproundness = Math.pow(Math.pow(Math.abs(params['ltoproundness'] * Math.sin(angle)), 2) + Math.pow(Math.abs(params['wtoproundness'] * Math.cos(angle)), 2),
1 / 2);
var points2d = get2DPointsForSlice(params['height'], r, baseheight, baseradius, rheight, params['roofheight'], params['oradius'], params['oheight'], params['opinch'], toproundness, params['lowroundness'], fullness, params['resolution']);
var points = [];
for (var i=0; i<points2d.length; i++) {
points.push([points2d[i][0] * Math.cos(angle), points2d[i][0] * Math.sin(angle), points2d[i][1]]);
}
return points;
}
function drawSlice(mesh, last_slice, next_slice, height, oheight) {
// Parameter slices MUST have the same size.
// The very first points connect to (0, 0, height).
mesh.triangle([0, 0, height], last_slice[0], next_slice[0]);
// The rest of the points make little boxes.
for (var i=0; i<next_slice.length-1; i++) {
mesh.triangle(next_slice[i], last_slice[i], last_slice[i+1]);
mesh.triangle(last_slice[i+1], next_slice[i+1], next_slice[i]);
}
// The very last points connect to (0, 0, oheight).
mesh.triangle(next_slice[next_slice.length - 1], last_slice[last_slice.length - 1], [0, 0, oheight]);
}
function process(params) {
var mesh = new Mesh3D();
var first_slice = get3DPointsForSlice(params, 0);
var last_slice = first_slice;
for (var i=1; i<params['resolution'] * 4; i++) {
var angle = 2.0 * Math.PI * i / (params['resolution'] * 4);
var slice = get3DPointsForSlice(params, angle);
drawSlice(mesh, last_slice, slice, params['height'], params['oheight']);
last_slice = slice;
}
// Now draw in the final slice connecting back to the first one.
drawSlice(mesh, last_slice, first_slice, params['height'], params['oheight']);
return Solid.make(mesh);
}
@theGOTOguy
Copy link
Author

theGOTOguy commented Apr 6, 2023

I made this shape and wrote this source code for the TinkerCad "bezier bean" custom shape generator. Please enjoy.

https://www.tinkercad.com/things/7ECRdzjIHsa-cloud-gate
https://www.thingiverse.com/thing:684126

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment