/** This prints a chain guide for a lightswitch. This is for 3-D printing. This model begins with two cylinders. One that points straight up/down for the pullchain to fall down, and one at the top that guides the chain toward the light fixture. These two cylinders are joined by a tricky bit: a curved tube that smoothly rotates (and potentially tapers) to exactly join the bottom cylinder and the top cylinder. The shape of the tricky joining tube is found by a few insights: 1.) The joining tube should be an annulus (ring) rotated as a solid of rotation. This will make a curved, hollow cylinder. 2.) The joining tube can be rotated around an essentially arbitrary point. 3.) The top end of the joining tube must match the angle of the top tube (that is, pointing to the light fixture) 4.) How do we find the axis to curve the solid of rotation around? Well, if we have two vectors, and want to find a third vector that is perpendicular to them both, that's what the cross product is for! 5.) How do we find the angle to rotate the solid of rotation around? Well, that's what the dot product is for! The arccos of the dot product gives an angle which is the angle to rotate the solid of rotation through. 6.) The cylinder functions like to be given the x,y,z coordinates of the centers of their endpoints. We can generate those coordinates by taking the endpoints of a cylinder as if it's sitting "upward" on the top of the bottom cylinder and then transforming those coordinates using a frink.graphics.CoordinateTransformer3DFloat */ res = 254/inch wallThickness = 3.21 mm baseHeight = .8 cm thinRadiusInner = 1.3/2 cm thinRadiusOuter = thinRadiusInner + wallThickness thickRadiusInner = 1.3/2 cm thickRadiusOuter = thickRadiusInner + wallThickness topHeight = .8 cm topRadiusInner = 1.5/2 cm topRadiusOuter = thickRadiusInner + wallThickness // Bottom cylinder baseOuter = callJava["frink.graphics.VoxelArray", "makeTaperedCylinder", [0,0,0,0,0, baseHeight res, thickRadiusOuter res, thinRadiusOuter res]] baseInner = callJava["frink.graphics.VoxelArray", "makeTaperedCylinder", [0,0,0,0,0, baseHeight res, thickRadiusInner res, thinRadiusInner res]] baseOuter.remove[baseInner] // Make a thin ring that will be rotated and extruded as a solid of rotation. ringOuter = callJava["frink.graphics.VoxelArray", "makeCylinder", [0,0,baseHeight res,0,0, (baseHeight+0.15 mm) res, thinRadiusOuter res]] ringInner = callJava["frink.graphics.VoxelArray", "makeCylinder", [0,0,baseHeight res,0,0, (baseHeight + 0.15 mm) res, thinRadiusInner res]] ringOuter.remove[ringInner] // A vector pointing straight up (this is the alignment of the bottom cylinder) vup = newJava["frink.graphics.Point3DFloat", [0,0,1]] // A vector pointing toward the light (the alignment of the upper cylinder) vlight = newJava["frink.graphics.Point3DFloat", [-4.25, 25.5, 17]].normalize[] // Find an axis that is perpendicular to both vup and vlight to rotate around // (that's precisely what the "cross product" operator does!) vaxis = vup.crossProduct[vlight] // Find the angle to rotate around to turn vup to vlight (that's precisely // what the arccos of the "dot product" operator does!) angle = arccos[vup.dotProduct[vlight]] // Arbitrary center point to rotate around center = newJava["frink.graphics.Point3DFloat", [-2 thinRadiusOuter res, 2 thinRadiusOuter res, (baseHeight) res]] println["Angle is " + format[angle,"deg",3]] // This generates the solid of rotation for the ring around the point // "center" and the rotation axis "vaxis" bend = ringOuter.solidOfRotation[center, vaxis, 0 deg, angle] // Make a coordinate transformer that will rotate points to calculate the ends // of the top cylinder. This makes a rotation transformation // around "vaxis" and passing through point "center", rotated by "angle" ct = callJava["frink.graphics.CoordinateTransformer3DFloat", "makeRotate", [center, vaxis, angle]] // Transform centers of the ends of the top cylinder. We do this by imagining // placing the top cylinder on top of the bottom cylinder and then transforming // the placement of its endpoints. rc1 = ct.transform[0, 0, baseHeight res, undef] rc2 = ct.transform[0, 0, (baseHeight+topHeight) res, undef] // Now render the top (tapered) cylinder. It gets wider toward the top. topOuter = callJava["frink.graphics.VoxelArray", "makeTaperedCylinder", [rc1, rc2, thinRadiusOuter res, topRadiusOuter res]] topInner = callJava["frink.graphics.VoxelArray", "makeTaperedCylinder", [rc1, rc2, thinRadiusInner res, topRadiusInner res]] topOuter.remove[topInner] v = baseOuter.union[bend] v = v.union[topOuter] // Mounting flange minX = (thickRadiusOuter-wallThickness) res maxX = thickRadiusOuter res minY = -thickRadiusOuter res maxY = .95 in res minZ = 0 maxZ = 1.43 in res flange = v.cube[minX, maxX, minY, maxY, minZ, maxZ, true] // Tapered countersunk screw holes for mounting // Top hole c1 = callJava["frink.graphics.VoxelArray", "makeTaperedCylinder", [minX, minY+4.5 mm res, maxZ-4.5 mm res,maxX, minY+4.5 mm res, maxZ-4.5 mm res, 5.8 mm/2 res, 3.0 mm/2 res]] // Bottom hole c2 = callJava["frink.graphics.VoxelArray", "makeTaperedCylinder", [minX, maxY-4.5 mm res, minZ+4.5 mm res, maxX, maxY-4.5 mm res, minZ+4.5 mm res, 5.8 mm/2 res, 3.0 mm/2 res]] flange.remove[c1] flange.remove[c2] v = v.union[flange] v.projectX[undef].show["X"] v.projectY[undef].show["Y"] v.projectZ[undef].show["Z"] filename = "chainGuide.obj" print["Writing $filename..."] w = new Writer[filename] w.println[v.toObjFormat["v", 1/(res mm)]] w.close[] println["done."]