/* Snap Eyeglasses Case, version 1.0 Copyright 2026 Tyler Tork This glasses case is parameterized using the Customizer to let you specify the dimensions of the glasses and other options, including text to include in a different color. This is designed to hold the glasses snugly (assuming you enter snug measurements) and allows one-handed operation of the opening mechanism. Licensed under Creative Commons - Attribution - Non-Commercial - Share Alike https://creativecommons.org/licenses/by-nc-sa/4.0/ */ // Show case open, closed, or laid out for printing. display = "open"; // [open,closed,print,top:top only,bottom:bottom only,label:text only,guide:sizing guide,debug] // cut cross sections for debugging section = false; /* [Glasses dimensions when folded] */ // side to side width = 144; // top to bottom height = 41; // when folded, how thick at thickest point. depth = 31; // when folded, thickness at bottom bottom_depth = 15; // wall thickness (except front lip which might be made thicker for strength) wall = 1.6; // [1:.1:3] // gap between overlapping top curves, so they don't scrape. leeway = .6; // [.5:.1:1] // diameter of conical hinge post, at base. diamPeg = 7; // [5:.5:10] /* [Magnets] */ // height, diameter. magnet_size = [1.8,5.3]; magnet_position = "middle"; // [corners,middle] // adjust for your printer's precision $slop = 0.14; /* [Text] */ text_on_outside = false; textLine1 = "Sample"; textLine2 = ""; text_depth = .2; fontSize = 8; fontFace = ""; /* [Hidden] */ include $fa=3; $fs = .5; EPS = .01; textlines = [ if (textLine1 !="") textLine1, if (textLine2!="") textLine2 ]; rTop = max(7, depth/2); // inner radius at hinge end. rBottom = max(7,bottom_depth/2); // inner radius at lip. rimWall = max(wall, min(2.7, 20/rBottom)); // lip thickness -- at least 'wall' but may be thicker if we calculate it needs extra strength. rimExtra = rimWall - wall; htInner = max(height, 30); // height of tray opening. wdInner = max(width, 130); // width ditto wdOuter = wdInner + 2*wall; // width of tray outside htOuter = htInner + 2*wall + rimExtra; // height ditto posAxle = [0, htInner-rTop+wall+rimExtra, 0]; // coordinates of rotation center for hinge placement. a = asin((rTop-rBottom)/(htInner-rTop-rBottom)); // angle between rim and bottom. wHinge = wdOuter + $slop*2 + 3; // widest point of blue part. wCaps = wHinge+2+2*$slop; // widest point of pink part. dC = htInner-rTop-rBottom; // distance between hinge pin and center of bottom curve. rRim = wall+rBottom+rimExtra; // radius of outer curve on lip. /* wedgit is the shape of the cross-section of the glasses case. 'inner=true' means we want the shape of the blue piece. The shape encloses two circles of radii r1 and r2 positioned with centers w-r1-r2 apart. The outermost 60 degrees of the circle edge is on the end of the shape, and the rest of the bottom edge is three lines - a long one tangent to both circles. - a tangent to the circle at 30 degrees below x axis on each end. The result is a shape that's easy to print but allows for a round hinged end with 60 degrees of motion. */ module wedgit(w, r1, r2, a=a, inner=true) { dC = w-r1-r2; r2p = inner ? r2 : r2 + leeway + wall; hull() { right(w-r2) { difference() { circle(r=r2p); if (!inner) zrot(-a) fwd(r2) square(r2p*2, anchor=TOP); } intersection() { zrot(inner?-150:-150) square(r2p); // # zrot(-150) square(r2p); zrot(-90-a) square(r2); } } intersection() { right(r1) { circle(r=r1); zrot(40) square(r1, anchor=TOP+RIGHT); } right(w-r2) zrot(-a) square([dC+r1, r2], anchor=TOP+RIGHT); } } } // generate the label text, positioned as if the inside of the tray is level at z=0. module label() { dep = max(0.1, min(wall-.45,text_depth)); echo("dep", dep); adj = $preview ? EPS : 0; down(text_on_outside ? wall+adj : dep) linear_extrude(dep + adj) xscale(text_on_outside ? -1 : 1) textLines(textlines, fontSize, font=fontFace, valign="bottom"); } // utility function to display multiple lines of text with speficied alignment. module textLines(lines, size=10, font="", halign="center", valign="baseline") { lineht = size*1.4; n = len(lines); oy = valign=="center"?(n-1)*lineht/2:valign=="top"?0:(n-1)*lineht; for(i = [0:len(lines)-1]) { translate([0, oy-lineht*i, 0]) text(lines[i], size=size, font=font, halign=halign, valign=valign); } } // return the shape of the magnet holder column, or the hole that holds the magnet (if hole=true). module magnet_post(hole = false) { if (hole) { up(EPS) cylinder(magnet_size[0]+$slop+EPS, d=magnet_size[1] + $slop, anchor=TOP); } else { cylinder(rBottom*2, d=magnet_size[1] + $slop+2, anchor=TOP); } } // return basically the shape of the top or bottom tray without the added bits for the hinge or magnets. This is used both for the exterior, and to scoop out a hole for the interior. // The part we want is below the XY plane and there's a bit on top to discard. module cylwedge(rTop=20, rBottom=10, l = 100, w = 50, inner=true) { rotate([90,0,90]) linear_extrude(l, center=true) wedgit(w, rBottom, rTop, a, inner); } // the shape that's common to both sides of the clamshell, including magnet posts but not the hinge parts or label. module tray(inner=true) { rPost = (magnet_size[1]+$slop)/2 + 1; difference() { // outer surface. cylwedge(l=wdOuter, rTop=rTop+wall, rBottom = rRim, w=htOuter, inner); // remove the top half of the cylwedge fwd(1) cube([wdOuter+2, htOuter*2, max(rTop,rBottom)*2], anchor=FRONT+BOTTOM); // subtract interior void except magnet posts. difference() { back(rimWall) cylwedge(l=wdInner, rTop=rTop, rBottom = rBottom, w=htInner, inner); if (magnet_position=="middle") { translate([0, rPost, 0]) magnet_post(); } else { for(d=[1,-1]) translate([d*(wdOuter/2-rPost), rPost, 0]) magnet_post(); } } if (magnet_position=="middle") { translate([0, rPost, 0]) magnet_post(true); } else { for(d=[1,-1]) translate([d*(wdOuter/2-rPost), rPost, 0]) magnet_post(true); } } } // returns a pie slice out of an empty can, positioned as anchor=BOTTOM. rIn is interior radius, rOut exterior radius, wall is the thickness of the end walls, len is the height of the can (exterior), angle is how many degrees of the can you want, starting at spin degrees clockwise from the X axis. module canSection(rIn, rOut, wall, len, angle, spin, rounding) { zrot(spin) intersection() { difference() { cyl(len, r=rOut, rounding=rounding); cyl(len-wall-wall, r=rIn, rounding=max(0, rounding-rOut+rIn)); } pie_slice(ang=angle, l=len+1, r=rOut+1, center=true); // up(wall) cylinder(len-2*wall, r=rIn); } } // the blue part of the case, with hinge pins and no label. module bottom() { htCyl = wall+$slop+1.5; difference() { union() { tray(); // hinge pegs for(x=[-.5,.5]) { translate(posAxle+[x*(wdOuter-wall-wall), 0, 0]) yrot(180*x) { cyl(h=htCyl, d=diamPeg, chamfer2 = 1.5+$slop, anchor=BOTTOM); } } } // little bevel at the back for a bit of extra clearance back(htOuter+EPS) xrot(45) cube([wdOuter+2*$slop,.71,.71], center=true); } } // the conical hinge covers module endcaps() { difference() { // make a long cylinder with a chamfer then cut off the unwanted parts. ch = (wCaps-wdOuter)/2-.5; // chamfer is for the top edge of the curve, which will have a .5mm flat rim so it's not sharp. d = min(rTop*2, max(diamPeg+7, (diamPeg+2*rTop)/2)); union() { // cylinder that shows above tray edge cyl(h=wCaps, chamfer=ch, d=d); // wider half-cylinder for down-facing side, to reach outer tray wall and prevent overhang. difference() { cyl(h=wCaps, d=d+1, chamfer=ch+.5); cube([d+2, d+2, wCaps+1], anchor=RIGHT); } } // subtract the middle of the endcaps that would be inside the tray. cylinder(wdOuter, r=rTop+1, center=true); // subtract a slightly wider section above tray edge to provide clearance for blue tray. cube([rTop+2, 100, wdOuter+2*$slop], anchor=RIGHT); } } // the pink part of the glasses case, with the hinge holes and label. module top() { difference() { union() { difference() { union() { // part of tray that matches bottom tray(false); // hinge-cover disks at end translate(posAxle) yrot(90) difference() endcaps(); } // subtractions: // leeway for top to rotate translate(posAxle) xrot(30) cube([wdOuter+2*$slop, 100, 100], anchor=FRONT); } // angled bits to prevent overhang of wider part of back wall. rr = rTop+wall+leeway + wall; for(d=[1,-1]) scale([d,1,1]) right(wdOuter/2) back(posAxle.y) xrot(-60) hull() { left(.01) cube([wall+$slop+.01,rr, .01], anchor=TOP+LEFT+FRONT); down(wall*1.4) cube([.1,rr, .1], anchor=TOP+RIGHT+FRONT); } // larger curved wall at outside of hinged end. translate(posAxle) yrot(-90) canSection(rTop+wall+leeway, rTop+leeway+wall+wall, wall, wdOuter+2*(wall+$slop), 60, 90, 0); } // "axle" hole. translate(posAxle) yrot(-90) cyl(h=wHinge+$slop*2, d=diamPeg+2*$slop, center=true, chamfer=(wHinge-wdOuter)/2); // flatten top surface k=rBottom+wall; back(k) xrot(-a) down(k) cube([1.5*wdOuter, htOuter, 10], anchor=TOP+FRONT); // little bevel at the back for a bit of extra clearance back(htOuter+leeway-EPS) xrot(45) cube([wdOuter+2*$slop,.71,.71], center=true); } } // dSection carves a series of slices out of the children it is passed, only if the section option has been selected (for debugging). module dSection() { difference() { children(); if (section) { back(posAxle.y) xrot(30) cube([wdOuter*2, htOuter*2, 6], anchor=BOTTOM); cube([40,200,200], anchor=RIGHT); } } } // create cross section of closed case for people to print as test that folded glasses will fit through. Also includes magnet hole to check that sizing. module sizing_guide() { mm = yrot(-a) * up(magnet_size.x + magnet_size.y*sin(a)/2+.5); difference() { union() { linear_extrude(3) { for(s=[1,-1]) { scale([1,s,1]) difference() { wedgit(htInner+6, rBottom+3, rTop+3, a, true); right(3) wedgit(htInner, rBottom, rTop, a, true); square([htInner+6, 30]); } } } difference() { multmatrix(mm) magnet_post(false); cube(100, anchor=TOP); right(3) cube(100, anchor=LEFT); } } multmatrix(mm) { magnet_post(true); cylinder(30, d=magnet_size.y-1.5, anchor=TOP); } } %back(max(rBottom,rTop)+12) textLines(["If glasses fit thru this","you should be okay.","Magnet sizing test","also included"], size=5, valign="baseline"); } mmPrint = up(rBottom+wall) * xrot(a) * fwd(rBottom+rimWall); mmClosed = yrot(180); mmOpen = xrot(-59, cp=posAxle) * mmClosed; mmNone = up(0); if (display=="debug") { /* code here varies depending which module we're debugging */ // wedgit(htOuter, rBottom, rTop, a, false); // up(.5) color("blue",.5) wedgit(htOuter, rBottom, rTop, a, true); dSection() top(); } else if (display == "guide") { sizing_guide(); } else { mmTop = (display=="print" || display=="top" || display=="label") ? back(htOuter+10) * mmPrint : display == "closed" ? mmClosed : mmOpen; mmBot = (display=="print" || display=="label" || display=="bottom" ) ? mmPrint : mmNone; if (display != "top" && display != "label") { render() color("lightblue") multmatrix(mmBot) dSection() bottom(); } if (display != "bottom" && display != "label") { render() color("pink") multmatrix(mmTop) dSection() top(); } if (display != "bottom" && display != "top") { color("black") multmatrix(mmTop) back(rimWall+rBottom) xrot(-a) back(max(2, magnet_position=="middle"?magnet_size.y/2+1:0)) down(rBottom) up(.001) label(); } }