Created
July 19, 2020 16:10
-
-
Save opencoca/99a03d848f94eb39dc1caead26e66be5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// title : keyboard | |
// author : Joost Nieuwenhuijse | |
// license : MIT License | |
// description: a lamp shade | |
// file : lamp-shade.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 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]); | |
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: 160, caption: 'Top diameter:'}, | |
{name: 'bottomdiameter', type: 'float', initial: 300, caption: 'Bottom diameter:'}, | |
{name: 'height', type: 'float', initial: 170, caption: 'Height:'}, | |
{name: 'numfaces', type: 'int', initial: 5, caption: 'Number of faces:'}, | |
{name: 'thickness', type: 'float', initial: 4, caption: 'Thickness of stock material:'}, | |
{name: 'topholediameter', type: 'float', initial: 42, caption: 'Diameter of top hole:'}, | |
{name: 'cutterdiameter', type: 'float', initial: 3.2, 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: '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 | |
} | |
]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment