Skip to content

Instantly share code, notes, and snippets.

@opencoca
Last active January 17, 2021 20:40
Show Gist options
  • Select an option

  • Save opencoca/7533f34b735dc4c31786aad0efca5eac to your computer and use it in GitHub Desktop.

Select an option

Save opencoca/7533f34b735dc4c31786aad0efca5eac to your computer and use it in GitHub Desktop.
A jscad script used to generate simple storage boxes.
// title : Box
// author : Alexander Somma
// license : MIT License
// description: A box for storing things
// file : Box.jscad
function main (params) {
CSG.defaultResolution2D = (params.quality === 'DRAFT') ? 8 : 32;
var bottomradius = params.bottomdiameter / 2;
var topradius = params.topdiameter / 2;
var height = params.height;
var numfaces = params.numfaces;
var thickness = params.thickness;
var topholeradius = params.topholediameter / 2;
var cutterRadius = params.cutterdiameter / 2;
var cutRandomHoles = params.cutrandomholes;
var solid = CSG.cube({radius: [1000, 1000, height / 2]});
var plane = CSG.Plane.fromPoints([bottomradius, 0, -height / 2], [bottomradius, 10, -height / 2], [topradius, 0, height / 2]);
for (var i = 0; i < numfaces; i++) {
solid = solid.cutByPlane(plane.rotateZ(i * 360 / numfaces));
}
var plates = solidToOuterShellPlates(solid, thickness);
plates = removePlateWithNormal(plates, [0, 0, -1]);
plates = removePlateWithNormal(plates, [0, 0, 1]);
for (var j = 1; j < numfaces; j++) {
plates[j] = plates[0].rotateZ(j * 360 / numfaces);
}
var topplate = getStockPlate(1000, 1000, thickness)
.subtract(CSG.cylinder({start: [0, 0, -thickness], end: [0, 0, thickness], radius: topholeradius}))
.translate([0, 0, height / 2 - thickness / 2 - 10]);
topplate = topplate.intersect(solid);
topplate = fixPlate(topplate, thickness);
var fingerjointoptions = {
margin: 0, cutterRadius: cutterRadius, fingerWidth: 25
};
plates = fingerJoint(plates, fingerjointoptions);
plates = fingerJointAdd(plates, topplate, fingerjointoptions);
if (params.type === 'TOPPLATE') {
return plateCSGToCAG(plates[numfaces]);
} else {
var plate2d = plateCSGToCAG(plates[0]);
if(cutRandomHoles === 'True'){
plate2d = addRandomHoles(plate2d);
}
if (params.type === 'SIDEPLATE') {
return plate2d;
} else {
for (var k = 0; k < numfaces; k++) {
var plate3d = plateCAGToCSG(plate2d, plates[k].properties.platebasis, thickness);
plates[k] = plate3d;
}
var result = new CSG().union(plates);
result = result.rotateX(90);
return result;
}
}
}
function addRandomHoles (plate) {
var distancefromedge = 8;
var distancebetweenholes = 10;
var mindiameter = 10;
var maxdiameter = 25;
// maskarea: the 'forbidden' area for holes:
var maskarea = plate.contract(distancefromedge, 4);
var bounds = maskarea.getBounds();
maskarea = maskarea.flipped();
var holes = [];
var existingholecenters = [];
var existingholeradii = [];
for (var i = 0; i < 10; i++) {
for (var tryindex = 0; tryindex < 10; tryindex++) {
var holeradius = (mindiameter + Math.random() * (maxdiameter - mindiameter)) / 2;
var x = bounds[0].x + holeradius + (bounds[1].x - bounds[0].x - holeradius * 2) * Math.random();
var y = bounds[0].y + holeradius + (bounds[1].y - bounds[0].y - holeradius * 2) * Math.random();
var holecenter = new CSG.Vector2D(x, y);
var valid = true;
// check if the hole is too close to one of the existing holes:
var numexistingholes = existingholecenters.length;
for (var i2 = 0; i2 < numexistingholes; i2++) {
var d = holecenter.minus(existingholecenters[i2]).length();
if (d < holeradius + existingholeradii[i2] + distancebetweenholes) {
valid = false;
break;
}
}
if (valid) {
// check if the hole is not too close to the edges:
var hole = CAG.circle({radius: holeradius, center: holecenter});
var testarea = maskarea.intersect(hole);
if (testarea.sides.length !== 0) valid = false;
if (valid) {
existingholeradii.push(holeradius);
existingholecenters.push(holecenter);
holes.push(hole);
break;
}
}
}
}
return plate.subtract(holes);
}
function plateCSGToCAG (plate) {
if (!('platebasis' in plate.properties)) {
throw new Error('Plates should be created using getStockPlate()');
}
var plate2d = plate.projectToOrthoNormalBasis(plate.properties.platebasis);
return plate2d;
}
function plateCAGToCSG (plate2d, platebasis, thickness) {
var basisinversematrix = platebasis.getInverseProjectionMatrix();
var plateReprojected = plate2d.extrude({offset: [0, 0, thickness]}).translate([0, 0, -thickness / 2]);
plateReprojected = plateReprojected.transform(basisinversematrix);
plateReprojected.properties.platebasis = platebasis;
return plateReprojected;
}
function fixPlate (plate, thickness) {
return plateCAGToCSG(plateCSGToCAG(plate), plate.properties.platebasis, thickness);
}
function removePlateWithNormal (plates, normalvector) {
normalvector = new CSG.Vector3D(normalvector);
var result = [];
plates.map(function (plate) {
if (!('platebasis' in plate.properties)) {
throw new Error('Plates should be created using getStockPlate()');
}
if (plate.properties.platebasis.plane.normal.dot(normalvector) < 0.9999) {
result.push(plate);
}
});
return result;
}
function getStockPlate (width, height, thickness) {
var result = CSG.cube({radius: [width / 2, height / 2, thickness / 2]});
result.properties.platebasis = CSG.OrthoNormalBasis.Z0Plane();
return result;
}
function fingerJointAdd (plates, newplate, options) {
var result = plates.slice(0);
var numplates = plates.length;
for (var plateindex1 = 0; plateindex1 < numplates; plateindex1++) {
var joined = fingerJointTwo(result[plateindex1], newplate, options);
result[plateindex1] = joined[0];
newplate = joined[1];
}
result.push(newplate);
return result;
}
// Finger joint between multiple plates:
function fingerJoint (plates, options) {
var result = plates.slice(0);
var numplates = plates.length;
var maxdelta = Math.floor(numplates / 2);
for (var delta = 1; delta <= maxdelta; delta++) {
for (var plateindex1 = 0; plateindex1 < numplates; plateindex1++) {
var plateindex2 = plateindex1 + delta;
if (plateindex2 >= numplates) plateindex2 -= numplates;
var joined = fingerJointTwo(result[plateindex1], result[plateindex2], options);
result[plateindex1] = joined[0];
result[plateindex2] = joined[1];
if (delta * 2 >= numplates) {
// numplates is even
if (plateindex1 * 2 >= numplates) {
// and we've done the first half: we're done
break;
}
}
}
}
return result;
}
function fingerJointTwo (plate1, plate2, options) {
if (!options) options = {};
if (!('platebasis' in plate1.properties)) {
throw new Error('Plates should be created using getStockPlate()');
}
if (!('platebasis' in plate2.properties)) {
throw new Error('Plates should be created using getStockPlate()');
}
// get the intersection solid of the 2 plates:
var intersection = plate1.intersect(plate2);
if (intersection.polygons.length === 0) {
// plates do not intersect. Return unmodified:
return [plate1, plate2];
} else {
var plane1 = plate1.properties.platebasis.plane;
var plane2 = plate2.properties.platebasis.plane;
// get the intersection line of the 2 center planes:
var jointline = plane1.intersectWithPlane(plane2);
// Now we need to find the two endpoints on jointline (the points at the edges of intersection):
// construct a plane perpendicular to jointline:
plane1 = CSG.Plane.fromNormalAndPoint(jointline.direction, jointline.point);
// make the plane into an orthonormal basis:
var basis1 = new CSG.OrthoNormalBasis(plane1);
// get the projection matrix for the orthobasis:
var matrix = basis1.getProjectionMatrix();
// now transform the intersection solid:
var intersectionTransformed = intersection.transform(matrix);
var bounds = intersectionTransformed.getBounds();
// now we know the two edge points. The joint line runs from jointlineOrigin, in the
// direction jointlineDirection and has a length jointlineLength (jointlineLength >= 0)
var jointlineOrigin = jointline.point.plus(jointline.direction.times(bounds[0].z));
var jointlineDirection = jointline.direction;
var jointlineLength = bounds[1].z - bounds[0].z;
var fingerwidth = options.fingerWidth || (jointlineLength / 4);
var numfingers = Math.round(jointlineLength / fingerwidth);
if (numfingers < 2) numfingers = 2;
fingerwidth = jointlineLength / numfingers;
var margin = options.margin || 0;
var cutterRadius = options.cutterRadius || 0;
var results = [];
for (var plateindex = 0; plateindex < 2; plateindex++) {
var thisplate = (plateindex === 1) ? plate2 : plate1;
// var otherplate = (plateindex === 1) ? plate1 : plate2;
// create a new orthonormal basis for this plate, such that the joint line runs in the positive x direction:
var platebasis = new CSG.OrthoNormalBasis(thisplate.properties.platebasis.plane, jointlineDirection);
// get the 2d shape of our plate:
var plate2d = thisplate.projectToOrthoNormalBasis(platebasis);
var jointlineOrigin2d = platebasis.to2D(jointlineOrigin);
matrix = platebasis.getProjectionMatrix();
intersectionTransformed = intersection.transform(matrix);
bounds = intersectionTransformed.getBounds();
var maxz = bounds[1].z;
var minz = bounds[0].z;
var maxy = bounds[1].y + margin / 2;
var miny = bounds[0].y - margin / 2;
var cutouts2d = [];
for (var fingerindex = 0; fingerindex < numfingers; fingerindex++) {
if ((plateindex === 0) && ((fingerindex & 1) === 0)) continue;
if ((plateindex === 1) && ((fingerindex & 1) !== 0)) continue;
var minx = jointlineOrigin2d.x + fingerindex * fingerwidth - margin / 2;
var maxx = minx + fingerwidth + margin;
var cutout = createRectCutoutWithCutterRadius(minx, miny, maxx, maxy, cutterRadius, plate2d);
cutouts2d.push(cutout);
}
var cutout2d = new CAG().union(cutouts2d);
var cutout3d = cutout2d.extrude({offset: [0, 0, maxz - minz]}).translate([0, 0, minz]);
cutout3d = cutout3d.transform(platebasis.getInverseProjectionMatrix());
var thisplateModified = thisplate.subtract(cutout3d);
results[plateindex] = thisplateModified;
}
return results;
}
}
// Create a rectangular cutout in 2D
// minx, miny, maxx, maxy: boundaries of the rectangle
// cutterRadius: if > 0, add extra cutting margin at the corners of the rectangle
// plate2d is the 2d shape from which the cutout will be subtracted
// it is tested at the corners of the cutout rectangle, to see if do need to add the extra margin at that corner
function createRectCutoutWithCutterRadius (minx, miny, maxx, maxy, cutterRadius, plate2d) {
var deltax = maxx - minx;
var deltay = maxy - miny;
var cutout = CAG.rectangle({radius: [(maxx - minx) / 2, (maxy - miny) / 2], center: [(maxx + minx) / 2, (maxy + miny) / 2]});
var cornercutouts = [];
if (cutterRadius > 0) {
var extracutout = cutterRadius * 0.2;
var hypcutterradius = cutterRadius / Math.sqrt(2.0);
var halfcutterradius = 0.5 * cutterRadius;
var dcx, dcy;
if (deltax > 3 * deltay) {
dcx = cutterRadius + extracutout / 2;
dcy = extracutout / 2;
} else if (deltay > 3 * deltax) {
dcx = extracutout / 2;
dcy = cutterRadius + extracutout / 2;
} else {
dcx = hypcutterradius - extracutout / 2;
dcy = hypcutterradius - extracutout / 2;
}
for (var corner = 0; corner < 4; corner++) {
var cutoutcenterx = (corner & 2) ? (maxx - dcx) : (minx + dcx);
var cutoutcentery = (corner & 1) ? (maxy - dcy) : (miny + dcy);
var cornercutout = CAG.rectangle({radius: [cutterRadius + extracutout / 2, cutterRadius + extracutout / 2], center: [cutoutcenterx, cutoutcentery]});
var testrectacenterx = (corner & 2) ? (maxx - halfcutterradius) : (minx + halfcutterradius);
var testrectbcenterx = (corner & 2) ? (maxx + halfcutterradius) : (minx - halfcutterradius);
var testrectacentery = (corner & 1) ? (maxy + halfcutterradius) : (miny - halfcutterradius);
var testrectbcentery = (corner & 1) ? (maxy - halfcutterradius) : (miny + halfcutterradius);
var testrecta = CAG.rectangle({radius: [halfcutterradius, halfcutterradius], center: [testrectacenterx, testrectacentery]});
var testrectb = CAG.rectangle({radius: [halfcutterradius, halfcutterradius], center: [testrectbcenterx, testrectbcentery]});
if ((plate2d.intersect(testrecta).sides.length > 0) && (plate2d.intersect(testrectb).sides.length > 0)) {
cornercutouts.push(cornercutout);
}
}
}
if (cornercutouts.length > 0) {
cutout = cutout.union(cornercutouts);
}
return cutout;
}
function solidToOuterShellPlates (csg, thickness) {
csg = csg.canonicalized();
var bounds = csg.getBounds();
var csgcenter = bounds[1].plus(bounds[0]).times(0.5);
var csgradius = bounds[1].minus(bounds[0]).length();
var plane2polygons = {};
csg.polygons.map(function (polygon) {
var planetag = polygon.plane.getTag();
if (!(planetag in plane2polygons)) {
plane2polygons[planetag] = [];
}
plane2polygons[planetag].push(polygon);
});
var plates = [];
for (var planetag in plane2polygons) {
var polygons = plane2polygons[planetag];
var plane = polygons[0].plane;
var shellcenterplane = new CSG.Plane(plane.normal, plane.w - thickness / 2);
var basis = new CSG.OrthoNormalBasis(shellcenterplane);
var inversebasisprojection = basis.getInverseProjectionMatrix();
var csgcenterProjected = basis.to2D(csgcenter);
var plate = getStockPlate(csgradius, csgradius, thickness).translate([csgcenterProjected.x, csgcenterProjected.y, 0]);
plate = plate.transform(inversebasisprojection);
plate = plate.intersect(csg);
plates.push(plate);
}
return plates;
}
function getParameterDefinitions () {
return [
{name: 'topdiameter', type: 'float', initial: 320, caption: 'Top diameter:'},
{name: 'bottomdiameter', type: 'float', initial: 320, caption: 'Bottom diameter:'},
{name: 'height', type: 'float', initial: 145, caption: 'Height:'},
{name: 'numfaces', type: 'int', initial: 4, caption: 'Number of faces:'},
{name: 'thickness', type: 'float', initial: 4, caption: 'Thickness of stock material:'},
{name: 'topholediameter', type: 'float', initial: 1, caption: 'Diameter of top hole:'},
{name: 'cutterdiameter', type: 'float', initial: 1, step: 0.1, caption: 'Diameter of CNC cutter / laser beam:'},
{
name: 'type',
type: 'choice',
values: ['ASSEMBLED', 'TOPPLATE', 'SIDEPLATE'], // these are the values that will be supplied to your script
captions: ['Assembled', 'Top plate (DXF output)', 'Side plate (DXF output)'], // optional, these values are shown in the listbox
// if omitted, the items in the 'values' array are used
caption: 'Show:', // optional, displayed left of the input field
initial: 'ASSEMBLED' // optional, default selected value
// if omitted, the first item is selected by default
},
{
name:'cutrandomholes',
type: 'choice',
values: ['True', 'False'], // these are the values that will be supplied to your script
captions: ['Yes', 'No'], // optional, these values are shown in the listbox
// if omitted, the items in the 'values' array are used
caption: 'Cut Random Holes:', // optional, displayed left of the input field
},
{
name: 'quality',
type: 'choice',
values: ['DRAFT', 'HIGH'], // these are the values that will be supplied to your script
captions: ['Draft', 'High'], // optional, these values are shown in the listbox
// if omitted, the items in the 'values' array are used
caption: 'Quality:', // optional, displayed left of the input field
initial: 'DRAFT' // optional, default selected value
// if omitted, the first item is selected by default
}
];
}
@opencoca

Copy link
Copy Markdown
Author

@opencoca

Copy link
Copy Markdown
Author

TODO add open from URL to OpenJSCAD.

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