/* Glass front picture frame with magnet mounts The model is a picture frame intended for mounting with magnets, for instance for small pictures to stick on a fridge. The assumption is that you will insert a rectangle of glass or (preferably) acrylic to protect the artwork. There are two printed parts, the "frame" which goes around the outside, and the "back" piece which snaps into the frame and can optionally be glued to it. The back piece contains configurable locations for magnets, and a hole to view the back of the artwork or a back-facing insert. A framed picture is a sort of sandwich, and this script lets you separately list the thicknesses of various layers, which it adds up to determine the total space needed to contain them snugly. The pieces are: - the glass - optional matting piece or other layers on top of the artwork. - artwork, which might wrap around the edges - optional insert behind the artwork, which doesn't wrap, intended to be visible through back aperture. - the backing piece that snaps into place. For each of these the user can enter a thickness, and the frame depth is calculated to just contain them all snugly. The backing piece thickness may end up being larger than specified because it has a minimum size to contain magnets, accommodate wrapped artwork, etc. If you specify that the artwork wraps, the frame will be colored transparent, since there's no point in wrapping if the flaps are hidden. An option on the "display" field creates a specialized tool to fold back the sides of your wrapping artwork to precisely fit around the back piece. This tool is the exact size of the back, so it has to be custom printed to match the frame dimensions. Because you might have multiples of these with small size variations, there's a parameter to specify text to engrave on the two parts of the tool (or you can write on them with a Sharpie :-) ). Copyright 2025 Tyler Tork Licensed under Creative Commons - Attribution - Non-Commercial - Share Alike https://creativecommons.org/licenses/by-nc-sa/4.0/ */ display = 0; // [0:for printing,1:assembled,2:sectioned,3:folding tool] /* [Front Matter (measurements in mm)] */ Glass_dimensions = [ 63.5, 69.85 ]; // 0.01 Glass_thickness = 1.4; // .01 Mat_thickness = 0; // .01 Art_thickness = .29; // .01 Wrap_sides = 0; // 0.1 Back_insert_thickness = .1; // 0.01 // How wide a strip of color will fold onto the sides? /* [Frame measurements (mm)] */ // How much of the glass edge should the frame cover? Frame_overlap = 1; // 0.1 // Side walls of outer frame (large if you want a thick frame border) Wall_thickness = 1.5; Frame_front_thickness = 1; // 0.1 /* [Back measurements (mm)] */ // Ridge and groove to clip it together. Ridge_height = 0.6; // Applies to all five sides of the backing piece. Back_wall_thickness = 1.5; Minimum_back_height = 2; // 0.1 // Size of rectangular opening to see back of artwork. Aperture_size = [0,0]; // Move opening from center by this distance. Aperture_offset = [0,0]; /* [Magnets] */ Magnet_style = 1; // [0:None, 1:Round, 2:Rectangular] Magnet_dimensions = [8,8,2]; // 0.1 Magnet_post_border = 0.7; // Extend magnet by how much past being flush with back piece. Magnet_extend = 0; // 0.1 Magnet1_position = [-999, -999]; Magnet2_position = [-999, -999]; Magnet3_position = [-999, -999]; Magnet4_position = [-999, -999]; magnetCoords = [Magnet1_position, Magnet2_position, Magnet3_position, Magnet4_position]; /* [Folding tool] */ // Text to write on folding tool; 'default' is glass dimensions, art thickness and wrap amount. Folding_tool_label = "default"; /* [Advanced] */ // the distance apart two lines of filament need to be to not smoosh together tolerance = 0.15; debug = false; /* [Hidden] */ $fa = 2; $fs = 0.2; include include use TOL = tolerance; EPS = 0.01; tkWrap = max(0, Art_thickness); htSides = tkWrap == 0 ? 0 : max(0, Wrap_sides); tkMat = max(0, Mat_thickness); tkInsert = max(0, Back_insert_thickness); dGlass = [max(30, Glass_dimensions.x), max(30, Glass_dimensions.y), max(0, Glass_thickness)]; htRidge = max(0.4, Ridge_height); tkFront = max(Frame_front_thickness, 0.4); tkFWall = max(.5, Wall_thickness); // wall must be thick enough to accommodate groove and then some. tkBWall = max(1, Back_wall_thickness); dMagnet = Magnet_dimensions; htMagnet = dMagnet.z; htMinPost = Magnet_style == 0 ? 0 : htMagnet-Magnet_extend; // if we're wrapping artwork around the side, the color is transparent. What's the point of wrapping if the edges are hidden? colFrame = htSides > 0 ? "#ffffc070" : "#4682b4"; htBack = max( Minimum_back_height, tkBWall, Magnet_style == 0 ? 0 : htMinPost+tkBWall, (htSides == 0 ? 0 : htSides+tkWrap-tkInsert)+2*htRidge+0.5 ); offsWrap = htSides > 0 ? tkWrap*2 : 0; dBack = [dGlass.x-offsWrap, dGlass.y-offsWrap, htBack]; //%cube(dBack, anchor=BOTTOM); dFrame = [dGlass.x + 2*(TOL + tkFWall), dGlass.y + 2*(TOL + tkFWall), htBack + tkInsert + tkMat + tkWrap + dGlass.z + tkFront+TOL]; lapFrame = max(0.4, Frame_overlap); /* Hold up a placard with an error message in the user's face. */ module notice(text) { lines = is_list(text) ? text : str_split(text, chr(10)); wMax = max([for (li=lines) textmetrics(li, 1).size[0]]); echo(wMax); sz = $vpd * $vpf / (80*wMax); offs = sz*(len(lines)-1)/2; translate($vpt) rotate($vpr) translate([0,0,$vpd/5]) { translate([0,0,-.01]) color("white") square([sz*wMax, sz*(1+len(lines))], center=true); color("darkred") for(i = [0:len(lines)-1]) translate([0,i*sz-offs,0]) text(lines[i], size=sz*.80, halign="center", valign="center"); } } /* A wedge is a prism shape used to make a snap-together ridge and valley. It's oriented with the ridge part pointing in the -x direction. */ module wedge(len = 10) { pts = [[EPS, EPS], [-htRidge*2-EPS, EPS], [-htRidge, -htRidge]]; yrot(-90) linear_extrude(height = len, center=true) polygon(pts); } /* A support for one magnet, returned BOTTOM anchored. */ module magnetPost(ind) { htPost = dBack.z-htMagnet+Magnet_extend; border = 2*(TOL+Magnet_post_border); if (Magnet_style == 1) { // cylinder if (Magnet_post_border > 0) { difference() { cylinder(htPost+1, d=dMagnet.x+border); up(htPost) cylinder(2, d=dMagnet.x+2*TOL); } } else { cylinder(htPost); } /* include ghost image of magnet */ // %up(htPost) cylinder(h=htMagnet, d=dMagnet.x); } else if (Magnet_style == 2) { if (Magnet_post_border > 0) { difference() { cube([dMagnet.x+border, dMagnet.y+border, htPost+1], anchor=BOTTOM); up(htPost) cube([dMagnet.x+2*TOL, dMagnet.y+2*TOL, 2], anchor=BOTTOM); } } else { cube([dMagnet.x, dMagnet.y, htPost], anchor=BOTTOM); } // %up(htPost) cube(dMagnet, anchor=BOTTOM); } } /* The narrow frame surrounding the outside of the model is returned with BOTTOM anchor in printing alignment, i.e. front facing downward. */ module frame() { dInner = dFrame-[tkFWall*2, tkFWall*2, 0]; extRidge = max(0, 0.5-tkFWall+htRidge); difference() { union() { // main box of frame cube(dFrame, anchor=BOTTOM); // if frame wall too thin for grove we're going to cut in it, make it a thicker at that point by adding a ridge to the outside. if (extRidge > 0) { up(dFrame.z-.5-htRidge) cuboid( [dFrame.x+2*extRidge, dFrame.y+2*extRidge, 2.5*extRidge], anchor=CENTER, chamfer=extRidge, edges="ALL"); } } // subtract the big oblong hole leaving the front and sides. up(tkFront) cube(dInner, anchor=BOTTOM); // cut a window in front to see the artwork through picHole = [dGlass.x-lapFrame*2, dGlass.y-lapFrame*2, 1+tkFront]; down(EPS) cube(picHole, anchor=BOTTOM); // bevel the corner of the picture aperture in front. bevel = max(0.5, tkFront-0.5); down(EPS) prismoid([picHole.x+EPS+2*bevel, picHole.y+EPS+2*bevel], [picHole.x-EPS, picHole.y-EPS], bevel+2*EPS); // make a long groove at inside center of each wall to clip the back into. up(dInner.z-0.5) { fwd(dInner.y/2) wedge(dBack.x/2+2); back(dInner.y/2) zrot(180) wedge(dBack.x/2+2); left(dInner.x/2) zrot(-90) wedge(dBack.y/2+2); right(dInner.x/2) zrot(90) wedge(dBack.y/2+2); } } } /* The clip-in backing piece is delivered with anchor=BOTTOM in printing orientation, i.e. front side down. */ module backpiece() { render() difference() { union() { // main block shape of back piece cube(dBack, anchor=BOTTOM); // if the art wraps around, make the back piece wider around the back rim by the thickness of the wrap, to fill the gap. if (htSides > 0) { htFlare = tkWrap+.5+htRidge*2; up(dBack.z-htFlare) cuboid([dBack.x+offsWrap, dBack.y+offsWrap, htFlare], anchor=BOTTOM, chamfer=tkWrap, edges=["Z", BOTTOM]); } } // hollow out the back to save material. up(tkBWall) cube(dBack-[tkBWall*2,tkBWall*2,0], anchor=BOTTOM); // if they asked for an aperture in back, cut it out. if(min(Aperture_size) > 0) { translate([Aperture_offset.x, Aperture_offset.y, -EPS]) cube([Aperture_size.x, Aperture_size.y, tkBWall+1], anchor=BOTTOM); } } // add the four ridges to clip into the frame. up(dBack.z-.5) { fwd((dBack.y+offsWrap)/2) wedge(dBack.x/2); back((dBack.y+offsWrap)/2) zrot(180) wedge(dBack.x/2); left((dBack.x+offsWrap)/2) zrot(-90) wedge(dBack.y/2); right((dBack.x+offsWrap)/2) zrot(90) wedge(dBack.y/2); } // if they asked for magnets, add the posts to hold them. if (Magnet_style != 0) { for(mc = magnetCoords) { if (abs(mc.x) < dBack.x/2 && abs(mc.y) < dBack.y/2) { translate([mc.x,mc.y,0]) magnetPost(); } } } } /* For previewing the assembled frame, add the edge-wrapped artwork in TOP anchoring. */ module wrapPreview() { if (htSides == 0) { cube([dGlass.x, dGlass.y, tkWrap], anchor=BOTTOM); } else { dW = [dGlass.x-2*tkWrap, dGlass.y-2*tkWrap, tkWrap]; cuboid([dW.x,dGlass.y,tkWrap], anchor=BOTTOM, rounding=tkWrap, edges=[TOP+FRONT,TOP+BACK]); cuboid([dGlass.x,dW.y,tkWrap], anchor=BOTTOM, rounding=tkWrap, edges=[TOP+LEFT,TOP+RIGHT]); fwd(dW.y/2+tkWrap/2) cube([dW.x, tkWrap, Wrap_sides], anchor=TOP); back(dW.y/2+tkWrap/2) cube([dW.x, tkWrap, Wrap_sides], anchor=TOP); left(dW.x/2+tkWrap/2) cube([tkWrap, dW.y, Wrap_sides], anchor=TOP); right(dW.x/2+tkWrap/2) cube([tkWrap, dW.y, Wrap_sides], anchor=TOP); } } /* For previewing, show all the parts as they would look assembled -- including the glass and artwork. */ module assembled() { color("white") difference() { up(dBack.z) xrot(180) backpiece(); // if there's a child object, it's a "cutting" object to subtract from each component to create a cross section. children(); } color(colFrame) difference() { up(dFrame.z) xrot(180) frame(); children(); } // add the glass pane if (dGlass.z > 0) { color("blue", .1) difference() { up(dBack.z+tkInsert+tkWrap+tkMat+EPS*2) cube(dGlass, anchor=BOTTOM); children(); } } // add the back insert if any if (tkInsert > 0) { color("darkgreen") difference() { // if the artwork wraps, shrink insert to accommodate that up(dBack.z) cube([dGlass.x-offsWrap, dGlass.y-offsWrap, tkInsert], anchor=BOTTOM); children(); } } // add wrapping artwork if any if (tkWrap > 0) { color("pink") difference() { up(dBack.z+tkInsert) wrapPreview(); children(); } } // add front layer if any if (tkMat > 0) { up(dBack.z+tkInsert+tkWrap) { color("#ae794b") difference() { cube([dGlass.x,dGlass.y,tkMat], anchor=BOTTOM); children(); } } } } module main() { if (display == 3) { if(htSides == 0) notice("Folding tool not needed if 'Wrap sides' is zero."); else { label1 = Folding_tool_label == "default" ? str("gf ", str_join(Glass_dimensions, " "), " ", htSides, " ", tkWrap) : Folding_tool_label; folding_tool( [dBack.x, dBack.y, htSides-tkInsert], label1, label1, tkWrap, TOL ); } } else if (display >= 1 && $preview) { render() assembled() { if (display == 2) { down(.5) cube(dFrame+[0,0,1]); } } } else { render() { color(colFrame) frame(); right(dFrame.x+10) backpiece(); } } } if (debug) { backpiece(); % translate([.1,.1,0]) folding_block( [dBack.x, dBack.y, htSides-tkInsert], "sample", TOL ); } else { main(); }