OpenJsCad demo: Parametric Lamp Shade
Source code
Below is the OpenJsCad script for this demo. To build your own models, create a .jscad script and use the
OpenJsCad parser
. For more information see the
OpenJsCad documentation
.
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 i = 1; i < numfaces; i++) { plates[i] = plates[0].rotateZ(i * 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 i = 0; i < numfaces; i++) { var plate3d = plateCAGToCSG(plate2d, plates[i].properties.platebasis, thickness); plates[i] = 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(); var 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 plate_reprojected = plate2d.extrude({offset: [0,0,thickness]}).translate([0,0,-thickness/2]); plate_reprojected = plate_reprojected.transform(basisinversematrix); plate_reprojected.properties.platebasis = platebasis; return plate_reprojected; } 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: var 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 intersection_transformed = intersection.transform(matrix); var bounds = intersection_transformed.getBounds(); // now we know the two edge points. The joint line runs from jointline_origin, in the // direction jointline_direction and has a length jointline_length (jointline_length >= 0) var jointline_origin = jointline.point.plus(jointline.direction.times(bounds[0].z)); var jointline_direction = jointline.direction; var jointline_length = bounds[1].z - bounds[0].z; var fingerwidth = options.fingerWidth || (jointline_length / 4); var numfingers=Math.round(jointline_length / fingerwidth); if(numfingers < 2) numfingers=2; fingerwidth = jointline_length / 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, jointline_direction); // get the 2d shape of our plate: var plate2d = thisplate.projectToOrthoNormalBasis(platebasis); var jointline_origin_2d = platebasis.to2D(jointline_origin); matrix = platebasis.getProjectionMatrix(); intersection_transformed = intersection.transform(matrix); bounds = intersection_transformed.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 = jointline_origin_2d.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 thisplate_modified = thisplate.subtract(cutout3d); results[plateindex] = thisplate_modified; } 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 csgcenter_projected = basis.to2D(csgcenter); var plate = getStockPlate(csgradius, csgradius, thickness).translate([csgcenter_projected.x, csgcenter_projected.y, 0]); plate = plate.transform(inversebasisprojection); plate = plate.intersect(csg); plates.push(plate); } return plates; } function getParameterDefinitions() { return [ {name: 'topdiameter', type: 'float', default: 160, caption: "Top diameter:"}, {name: 'bottomdiameter', type: 'float', default: 300, caption: "Bottom diameter:"}, {name: 'height', type: 'float', default: 170, caption: "Height:"}, {name: 'numfaces', type: 'int', default: 5, caption: "Number of faces:"}, {name: 'thickness', type: 'float', default: 4, caption: "Thickness of stock material:"}, {name: 'topholediameter', type: 'float', default: 42, caption: "Diameter of top hole:"}, {name: 'cutterdiameter', type: 'float', default: 3.2, 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 default: "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 default: "DRAFT", // optional, default selected value // if omitted, the first item is selected by default }, ]; }