The Smell of Molten Projects in the Morning

Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.

Tag: M2

Using and tweaking a Makergear M2 3D printer

  • Chocolate Molds: Software Stack

    This derives directly from the cookie cutter / press stack, so check that series for more background and explanation. Some height map thoughts and preliminary doodling led up to this.

    We start with a tiny grayscale image file that defines the height of each point in the mold:

    Tux
    Tux

    Feed that file into a Bash script:

    ./MakeMold.sh Tux.png

    And a corresponding STL file pops out:

    Tux positive mold - solid model - oblique
    Tux positive mold – solid model – oblique

    The MakeMold Bash script orchestrates the whole thing:

    #!/bin/bash
    DotsPerMM=3.0
    MapHeight=5
    ImageName="${1%%.*}"
    rm ${ImageName}_* ${ImageName}-positive.stl
    echo Normalize and prepare grayscale image...
    convert $1 -type Grayscale -depth 8 -trim +repage -flip +set comment ${ImageName}_prep.png
    echo Create PGM files...
    convert ${ImageName}_prep.png -compress none ${ImageName}_map.pgm
    convert ${ImageName}_prep.png -white-threshold 1 -compress none ${ImageName}_plate.pgm
    echo Create height map data files...
    ImageX=`identify -format '%[fx:w]' ${ImageName}_map.pgm`
    ImageY=`identify -format '%[fx:h]' ${ImageName}_map.pgm`
    echo Width: ${ImageX} x Height: ${ImageY}
    cat ${ImageName}_map.pgm   | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*$ImageX)) > ${ImageName}_map.dat
    cat ${ImageName}_plate.pgm | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*$ImageX)) > ${ImageName}_plate.dat
    echo Create mold positive...
    time openscad -D fnPlate=\"${ImageName}_plate.dat\" \
    -D fnMap=\"${ImageName}_map.dat\" -D Height=$MapHeight \
    -D ImageX=$ImageX -D ImageY=$ImageY -D DotsPerMM=$DotsPerMM \
    -o ${ImageName}-positive.stl MoldPositive.scad
    

    The first convert normalizes the grayscale file and produces a PNG file in a standard format.

    The next two convert operations translate that PNG file into uncompressed PGM files with the data as ASCII text required by OpenSCAD’s surface() function. It’s not in the proper format, however, so a few lines of Bash-fu rearrange the data into DAT files; the extension is arbitrary.

    Then OpenSCAD eats those files along with a bunch of configuration settings and spits out a solid model of the positive mold in STL format.

    The MakePositive.scad OpenSCAD source code:

    // Mold positive pattern from grayscale height map using Minkowski sum
    // Ed Nisley KE4ZNU - February 2014 - adapted from cookie press, added alignment pins
    
    //-----------------
    // Mold files
    
    fnMap = "SqWr_map.dat";					// override with -D 'fnMap="whatever.dat"'
    fnPlate = "SqWr_plate.dat";				// override with -D 'fnPlate="whatever.dat"'
    
    DotsPerMM = 3.0;						// overrride with -D DotsPerMM=number
    
    MapHeight = 5.0;						// overrride with -D MapHeight=number
    
    ImageX = 100;							// overrride with -D ImageX=whatever
    ImageY = 100;
    
    MapScaleXYZ = [1/DotsPerMM,1/DotsPerMM,MapHeight/255];
    PlateScaleXYZ = [1/DotsPerMM,1/DotsPerMM,1.0];
    
    echo("Press File: ",fnMap);
    echo("Plate File: ",fnPlate);
    
    echo(str("ImageX:",ImageX," ImageY: ", ImageY));
    echo(str("Map Height: ",MapHeight));
    echo(str("Dots/mm: ",DotsPerMM));
    echo(str("Scale Map: ",MapScaleXYZ,"  Plate: ",PlateScaleXYZ));
    
    //- Extrusion parameters - must match reality!
    
    ThreadThick = 0.25;
    ThreadWidth = 2.0 * ThreadThick;
    
    //- Buid parameters
    
    PlateThick = IntegerMultiple(1.0,ThreadThick);		// solid plate under press relief
    
    PinOD = 1.75;				// locating pin diameter
    PinDepth = PlateThick;		//  ... depth into bottom surface = total length/2
    PinOC = 20.0;				// spacing within mold item
    
    echo(str("Pin depth: ",PinDepth," spacing: ",PinOC));
    
    //- Useful info
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    HoleWindage = 0.2;
    
    Protrusion = 0.1;						// make holes & unions work correctly
    
    MaxConvexity = 5;						// used for F5 previews in OpenSCAD GUI
    
    ZFuzz = 0.2;							// numeric chaff just above height map Z=0 plane
    
    //-----------------
    // Import plate height map, slice off a slab to define outline
    
    module Slab(Thick=1.0) {
    	intersection() {
    		translate([0,0,Thick/2])
    			cube([2*ImageX,2*ImageY,Thick],center=true);
    		scale(PlateScaleXYZ)
    			difference() {
    				translate([0,0,-ZFuzz])
    					surface(fnPlate,center=true,convexity=MaxConvexity);
    				translate([0,0,-1])
    					cube([2*ImageX,2*ImageY,2],center=true);
    			}
    	}
    }
    
    //- Put peg grid on build surface
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
    	Range = floor(50 / Space);
    
    	for (x=[-Range:Range])
    	  for (y=[-Range:Range])
    		translate([x*Space,y*Space,Size/2])
    			%cube(Size,center=true);
    }
    
    //-- convert cylinder to low-count polygon
    
    module PolyCyl(Dia,Height,ForceSides=0) {			// based on nophead's polyholes
    
      Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    
      FixDia = Dia / cos(180/Sides);
    
      cylinder(r=(FixDia + HoleWindage)/2,
               h=Height,
    	   $fn=Sides);
    }
    
    //-- Locating pin hole with glue recess
    //  Default length is two pin diameters on each side of the split
    
    module LocatingPin(Dia=PinOD,Len=0.0) {
    
    	PinLen = (Len != 0.0) ? Len : (4*Dia);
    
    	translate([0,0,-ThreadThick])
    		PolyCyl((Dia + 2*ThreadWidth),2*ThreadThick,4);
    
    	translate([0,0,-2*ThreadThick])
    		PolyCyl((Dia + 1*ThreadWidth),4*ThreadThick,4);
    
    	translate([0,0,-(Len/2 + ThreadThick)])
    		PolyCyl(Dia,(Len + 2*ThreadThick),4);
    
    }
    
    //- Build it
    
    //ShowPegGrid();
    
    echo("Building mold");
    union() {
    	difference() {
    		Slab(PlateThick + Protrusion);
    		for (i=[-1,1])
    			translate([0,i*PinOC/2,0])
    				rotate(180/4) LocatingPin(Len=2*PinDepth);
    	}
    	translate([0,0,PlateThick])							// cookie press height map
    		scale(MapScaleXYZ)
    		difference() {
    			translate([0,0,-ZFuzz])
    				surface(fnMap,center=true,convexity=MaxConvexity);
    			translate([0,0,-1])
    				cube([2*ImageX,2*ImageY,2],center=true);
    		}
    }
    
    

    The molds have alignment pin holes in the back:

    Tux positive mold - solid model - backside
    Tux positive mold – solid model – backside

    That match up with the holes in a baseplate:

    SqWr Positive Mold Framework - 2x3 pinsThe plate holds the molds in place, perhaps with tapeless sticky, while you’re slathering silicone goop to make the negative mold:

    Tux Positive Mold Framework - 2x3 array
    Tux Positive Mold Framework – 2×3 array

    As you might expect, the OpenSCAD file that generates the plate-with-holes can also embed the positive molds atop the plate, so you could get a solid (well, infilled at 20%) chunk of plastic without attaching the molds. I’d rather do the plate separately from the molds, so you can recycle the plate for many different molds. Your mileage may vary.

    The Positive Mold Framework.scad OpenSCAD source code:

    // Positive mold framework for chocolate slabs
    // Ed Nisley - KE4ZNU - January 2014
    
    Layout = "FramePins";		// FramePins FrameMolds Pin
    
    //- Extrusion parameters must match reality!
    //  Print with 2 shells and 3 solid layers
    
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    
    Protrusion = 0.1;			// make holes end cleanly
    
    HoleWindage = 0.2;
    
    //----------------------
    // Dimensions
    
    FileName = "Tux-positive.stl";	// overrride with -D
    
    Molds = [2,3];					// count of molds within framework
    
    MoldOC = [40.0,45.0];			// on-center spacing of molds
    MoldSlab = 1.0;					// thickness of slab under molds
    
    BaseThick = 5.0;
    
    BaseSize = [(Molds[0]*MoldOC[0] + 0),(Molds[1]*MoldOC[1] + 0),BaseThick];
    echo(str("Overall base: ",BaseSize));
    
    PinOD = 1.75;					// locating pin diameter
    PinLength = 2.0;				//  ... total length
    PinOC = 20.0;				// spacing within mold item
    
    //----------------------
    // Useful routines
    
    //- Put peg grid on build surface
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
    	RangeX = floor(100 / Space);
    	RangeY = floor(125 / Space);
    
    	for (x=[-RangeX:RangeX])
    		for (y=[-RangeY:RangeY])
    			translate([x*Space,y*Space,Size/2])
    			%cube(Size,center=true);
    
    }
    
    module PolyCyl(Dia,Height,ForceSides=0) {			// based on nophead's polyholes
    
      Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    
      FixDia = Dia / cos(180/Sides);
    
      cylinder(r=(FixDia + HoleWindage)/2,
               h=Height,
    	   $fn=Sides);
    }
    
    // Locating pin hole with glue recess
    //  Default length is two pin diameters on each side of the split
    
    module LocatingPin(Dia=PinOD,Len=0.0) {
    
    	PinLen = (Len != 0.0) ? Len : (4*Dia);
    
    	translate([0,0,-ThreadThick])
    		PolyCyl((Dia + 2*ThreadWidth),2*ThreadThick,4);
    
    	translate([0,0,-2*ThreadThick])
    		PolyCyl((Dia + 1*ThreadWidth),4*ThreadThick,4);
    
    	translate([0,0,-(Len/2 + ThreadThick)])
    		PolyCyl(Dia,(Len + 2*ThreadThick),4);
    
    }
    
    module LocatingPins(Length) {
    	for (i=[-1,1])
    	translate([0,i*PinOC/2,0])
    		rotate(180/4)
    		LocatingPin(Len=Length);
    }
    
    //-- import a single mold item
    
    module MoldItem() {
    	import(FileName,convexity=10);
    }
    
    //-- Overall frame shape
    
    module Frame() {
    
    //	translate([0,0,BaseSize[2]/2])		// platform under molds
    //		cube(BaseSize,center=true);
    
    	difference() {
    		hull()
    			for (i=[-1,1], j=[-1,1])
    				translate([i*BaseSize[0]/2,j*BaseSize[1]/2,0])
    					sphere(r=BaseThick);
    		translate([0,0,-BaseThick])
    			cube(2*BaseSize,center=true);
    	}
    
    }
    
    //- Build it
    
    ShowPegGrid();
    
    if (Layout == "Pin")
    	LocatingPin(Len=PinLength);
    
    if (Layout == "Frame")
    	Frame();
    
    if (Layout == "FramePins")
    	difference() {
    		Frame();
    
    		translate([-MoldOC[0]*(Molds[0] - 1)/2,-MoldOC[1]*(Molds[1] - 1)/2,0])
    			for (i=[0:Molds[0]-1],j=[0:Molds[1]-1])
    				translate([i*MoldOC[0],j*MoldOC[1],BaseSize[2]])
    					LocatingPins(BaseThick);
    	}
    
    if (Layout == "FrameMolds") {
    	Frame();
    	translate([-MoldOC[0]*(Molds[0] - 1)/2,-MoldOC[1]*(Molds[1] - 1)/2,0])
    		for (i=[0:Molds[0]-1],j=[0:Molds[1]-1])
    			translate([i*MoldOC[0],j*MoldOC[1],BaseThick - MoldSlab + Protrusion])
    			MoldItem();
    }
    

    And then it’s time to pour some chocolate… which someone else knows how to do much better than I!

  • Chocolate Mold: Image Preparation Checklist

    Start with a monochrome image:

    Tux-Shirt
    Tux-Shirt

    A bit of tinkering produce a height map image:

    Tux Mold - Height Map - large
    Tux Mold – Height Map – large

    I picked a 3.0 pixel/mm scale factor, so a 33 mm mold covers only 100 pixels. That image is 1100 mm tall and will be reduced by a factor of 10 to the final image size: this is not the place for fine detail and fancy lettering!

    The conversion process assumes you’ll handle the Z axis scaling yourself, so the script no longer normalizes the gray levels. If you select gray levels using HSV, the V slider gives you a direct reading in percent-of-maximum thickness; Tux varies from V = 80 to 100, so he’s pretty much bas relief.

    The border around the image must be 0 = black and will be stripped from the final mold. That’s why Tux doesn’t turn into a bird served on a rectangular platter.

    Because this is a mold, its edges must have some draft, which means the outline must shade from black to whatever gray represents the interior of the mold. Do this:

    • Trace the outline using the Scissors Select tool = snap to high-contrast outer edge
    • Create / go to a new layer filled with whatever gray you want for the interior (V = 80 here)
    • Select → Grow the selection by 60 pixels (on a 1000×1100 image)
    • Select → Invert to select the exterior of the outline
    • Bucket fill the exterior with 0 = black
    • Select  → Invert to select the interior of the outline again
    • Select → Border: Add a 30 pixel border to the selection with the “Feather border” option
    • Bucket fill the border with 0 = black
    • Unselect and you have a layer with a nice graduation around the mold

    Which looks like this with V=80 gray inside:

    Tux Mold - Height Map - outline gradient
    Tux Mold – Height Map – outline gradient

    The 30 pixel feathered border, scaled by the 10× reduction, means the edge of the mold goes from 0 = black to the interior in about 3 pixel / (3 pixel/mm) = 1 mm. If the interior is 255 = white at 7 mm, the draft angle is arctan 1/7 = 8°, which is probably about right for the deepest part of the mold. The edge of the Tux mold is V = 80 (or about 200 gray), so it’s at 0.8 × 7 mm = 5.6 mm and the draft angle is arctan 1/5.6 = 10°.

    Inside the mold, anything goes, but you should avoid 0 = black levels so that the alignment pins don’t poke through the mold. Any 255 = 100 V = white levels will be the maximum mold thickness, which is 7 mm for the molds you see here and that may be somewhat too thick for a chocolate treat. It is really hard to maintain draft on small features, but I think if you don’t get carried away it’ll be all good.

    There’s also a 1 mm backing plate below the mold that ensures the deepest mold parts have some substance behind them and the alignment pin sockets have enough depth to be useful.

    Scaling the image down by 10× to about 110 pixels tall (including the black border) will make the final Tux mold about 37 mm tall:

    Tux
    Tux

    This image enlarges it by 10× with no smoothing to show the gritty nature of the image. This is why you can’t have delicate detail or fine lettering:

    Tux - enlarged to show texture
    Tux – enlarged to show texture

    Notice the nearly complete lack of draft on the interior features. Each level differs by about V = 5 over the range V = 80(the border) to V = 100 (beak and flipper), so they amount to only 0.05 × 7 mm =  0.35 mm = one or two thread layers at 0.20 mm/layer. I think if you were doing this right, you’d pick an overall thickness so that V = 5 increments corresponded to one layer or use whatever V increments corresponded to a single layer.

    Running that image through the Bash script & OpenSCAD programs (more on those later) produces a reasonable result:

    Tux positive mold - solid model - oblique
    Tux positive mold – solid model – oblique

    When it’s converted into plastic, you can count the layers in each V = 5 level (clicky for more dots):

    Tux positive mold - plastic - oblique
    Tux positive mold – plastic – oblique

    It may be a bit less rounded in the tummy than the real Tux, but seems good enough for the purpose.

  • Chocolate Mold Array: Solid Model Doodling

    Given an STL file generated from a height map image, import it into OpenSCAD:

    SqWr solid model - OpenSCAD - oblique view
    SqWr solid model – OpenSCAD – oblique view

    Then slide a plate under six copies to produce a positive model for a casting mold:

    SqWr Positive Mold Framework - 2x3
    SqWr Positive Mold Framework – 2×3

    This is one of the few cases where the compiled-and-rendered version looks better, as though you’d shrink-wrapped it in gold foil:

    SqWr Positive Mold Framework - 2x3 - gold
    SqWr Positive Mold Framework – 2×3 – gold

    The height map STLs each have  a bazillion tiny facets that take forever-and-a-day (well, the better part of half an hour for this set) to render, not to mention that the whole array would take two hours to print… and then be used once or twice to produce the flexy silicone negative mold.

    So it’s better to have a generic frame with alignment pin holes that you print once:

    SqWr Positive Mold Framework - 2x3 pins
    SqWr Positive Mold Framework – 2×3 pins

    Better yet, just CNC-drill those holes in a nice, flat acrylic / polycarbonate slab.

    Insert and glue filament snippets as alignment pins, trim about 1 mm over the surface to fit the small molds.

    The OpenSCAD program can punch matching holes in the back of the small mold:

    SqWr solid model - OpenSCAD - oblique bottom
    SqWr solid model – OpenSCAD – oblique bottom

    Or you could print out an array of the things with holes:

    SqWr solid model - 2x3 array - bottom
    SqWr solid model – 2×3 array – bottom

    It’s not clear having OpenSCAD labor for half an hour to generate and emit a single STL file spanning all six molds is a win. Given that you don’t care about the mold-to-mold spacing, having Slic3r duplicate the same small STL file half a dozen (or more!) times would probably be a net win.

    There’s no reason the OpenSCAD program that creates the original STL from the height map image can’t punch alignment pin holes, too, which would avoid this import-and-recompile step. If you’re going with a CNC-drilled plate, then it would make even more sense to not have a pair of OpenSCAD programs.

    Anyhow.

    Apply a handful of small molds to the backing plate with tapeless sticky, butter it up with mold release agent, slather on silicone putty, flip it over to produce a smooth surface “under” the small molds (so you can rest it flat on a table when pouring molten chocolate into the cavities), cure, peel, and you’d get a pretty good negative mold.

    This may not make any practical sense, but it was easy & fun to see what’s possible…

    The OpenSCAD source code:

    // Positive mold framework for chocolate slabs
    // Ed Nisley - KE4ZNU - January 2014
    
    Layout = "FramePins";		// Molds FramePins FrameMolds Frame Single Pin
    
    //- Extrusion parameters must match reality!
    //  Print with 2 shells and 3 solid layers
    
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    
    Protrusion = 0.1;			// make holes end cleanly
    
    HoleWindage = 0.2;
    
    //----------------------
    // Dimensions
    
    FileName = "SqWr-press.stl";	// overrride with -D
    
    Molds = [2,3];					// count of molds within framework
    
    MoldOC = [40.0,40.0];			// on-center spacing of molds
    MoldSlab = 1.0;					// thickness of slab under molds
    
    BaseThick = 5.0;
    
    BaseSize = [(Molds[0]*MoldOC[0] + 0),(Molds[1]*MoldOC[1] + 0),BaseThick];
    echo(str("Overall base: ",BaseSize));
    
    PinOD = 1.75;					// locating pin diameter
    PinLength = 2.0;				//  ... total length
    PinSpace = 15.0;				// spacing within mold item
    
    //----------------------
    // Useful routines
    
    //- Put peg grid on build surface
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
    	RangeX = floor(100 / Space);
    	RangeY = floor(125 / Space);
    
    	for (x=[-RangeX:RangeX])
    		for (y=[-RangeY:RangeY])
    			translate([x*Space,y*Space,Size/2])
    			%cube(Size,center=true);
    
    }
    
    module PolyCyl(Dia,Height,ForceSides=0) {			// based on nophead's polyholes
    
      Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    
      FixDia = Dia / cos(180/Sides);
    
      cylinder(r=(FixDia + HoleWindage)/2,
               h=Height,
    	   $fn=Sides);
    }
    
    // Locating pin hole with glue recess
    //  Default length is two pin diameters on each side of the split
    
    module LocatingPin(Dia=PinOD,Len=0.0) {
    
    	PinLen = (Len != 0.0) ? Len : (4*Dia);
    
    	translate([0,0,-ThreadThick])
    		PolyCyl((Dia + 2*ThreadWidth),2*ThreadThick,4);
    
    	translate([0,0,-2*ThreadThick])
    		PolyCyl((Dia + 1*ThreadWidth),4*ThreadThick,4);
    
    	translate([0,0,-(Len/2 + ThreadThick)])
    		PolyCyl(Dia,(Len + 2*ThreadThick),4);
    
    }
    
    module LocatingPins(Length) {
    	for (i=[-1,1])
    	translate([i*PinSpace/2,0,0])
    		LocatingPin(Len=Length);
    }
    
    //-- import a single mold item
    
    module MoldItem() {
    	import(FileName,convexity=10);
    }
    
    //-- Overall frame shape
    
    module Frame() {
    
    	translate([0,0,BaseSize[2]/2])		// platform under molds
    		cube(BaseSize,center=true);
    
    }
    
    //- Build it
    
    ShowPegGrid();
    
    if (Layout == "Pin")
    	LocatingPin(Len=PinLength);
    
    if (Layout == "Single")
    	difference() {
    		MoldItem();
    		LocatingPins(PinLength);
    	}
    
    if (Layout == "Frame")
    	Frame();
    
    if (Layout == "Molds") {
    	translate([-MoldOC[0]*(Molds[0] - 1)/2,-MoldOC[1]*(Molds[1] - 1)/2,0])
    	for (i=[0:Molds[0]-1],j=[0:Molds[1]-1])
    		translate([i*MoldOC[0],j*MoldOC[1],0])
    			difference() {
    				MoldItem();
    				LocatingPins(PinLength);
    			}
    }
    
    if (Layout == "FramePins")
    	difference() {
    		Frame();
    
    		translate([-MoldOC[0]*(Molds[0] - 1)/2,-MoldOC[1]*(Molds[1] - 1)/2,0])
    			for (i=[0:Molds[0]-1],j=[0:Molds[1]-1])
    				translate([i*MoldOC[0],j*MoldOC[1],BaseSize[2]])
    					LocatingPins(BaseThick);
    	}
    
    if (Layout == "FrameMolds") {
    	Frame();
    	translate([-MoldOC[0]*(Molds[0] - 1)/2,-MoldOC[1]*(Molds[1] - 1)/2,0])
    		for (i=[0:Molds[0]-1],j=[0:Molds[1]-1])
    			translate([i*MoldOC[0],j*MoldOC[1],BaseThick - MoldSlab + Protrusion])
    			MoldItem();
    }
    
  • Chocolate Mold Height Map

    Given that you really don’t care about the absolute dimensions, you can generate a positive mold from a height map image and avoid the entire solid modeling process. Having already solved the cookie press problem, this was a quick-and-easy feasibility study…

    Start by selecting the logo, growing the selection by a few pixels, and feathering the edges to produce the mold draft. Then apply a square gradient behind the Squidwrench logo to produce the height map for the edge of the mold. This one is scaled at 3.0 pixel/mm and is 100×100 pixel, thus producing a 33 mm square mold:

    Squidwrench Mold Pocket

    One could, of course, produce a non-square mold with a different gradient outline shape.

    Hand the image to a slightly modified version of the cookie press script (see below) to get an STL file of the mold:

    SqWr solid model - oblique view
    SqWr solid model – oblique view

    Feed the STL into Slic3r, hand the G-Code to Pronterface, fire the M2!, and you get a positive mold that looks enough like black chocolate to seem ready-to-eat:

    SqWr - mold positive
    SqWr – mold positive

    I have no idea whether that will work as a mold, but I suspect flexy silicone putty won’t reproduce much of the fine plastic filament detail, so the negative mold won’t grab the chocolate. The logo is six threads deep with a little bit of draft, if that makes any difference.

    The backing plate is 1 mm thick and the height map is 5 mm stacked atop that. A few iterations suggested using about 0.75 gray for the logo; working backwards says 5 mm = 25 layers @ 0.20 mm/layer, so a depth of 0.25 * 25 is about six threads.

    For production use, I’d be tempted to import maybe a dozen copies of the STL into OpenSCAD, mount them on a platform with a gutter and a lip on the outside, and then print the whole positive multi-cavity mold in one shot.

    The Bash script that produces the mold strongly resembles my cookie cutter script and contains about as much cruft as you’d expect. Because we need a positive mold, not a negative press, the script doesn’t invert the colors or flop the image left-to-right, nor does it generate the cookie cutter STL around the outside of the press:

    #!/bin/bash
    DotsPerMM=3.0
    MapHeight=7
    ImageName="${1%%.*}"
    rm ${ImageName}_* ${ImageName}-press.stl ${ImageName}-cutter.stl
    echo Normalize and prepare grayscale image...
    convert $1 -type Grayscale -depth 8 -auto-level -trim +repage -flip +set comment ${ImageName}_prep.png
    echo Create PGM files...
    convert ${ImageName}_prep.png -compress none ${ImageName}_map.pgm
    convert ${ImageName}_prep.png -white-threshold 1 -compress none ${ImageName}_plate.pgm
    echo Create height map data files...
    ImageX=`identify -format '%[fx:w]' ${ImageName}_map.pgm`
    ImageY=`identify -format '%[fx:h]' ${ImageName}_map.pgm`
    echo Width: ${ImageX} x Height: ${ImageY}
    cat ${ImageName}_map.pgm   | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*$ImageX)) > ${ImageName}_map.dat
    cat ${ImageName}_plate.pgm | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*$ImageX)) > ${ImageName}_plate.dat
    echo Create cookie press...
    time openscad -D BuildPress=true \
    -D fnPlate=\"${ImageName}_plate.dat\" \
    -D fnMap=\"${ImageName}_map.dat\" -D Height=$MapHeight \
    -D ImageX=$ImageX -D ImageY=$ImageY -D DotsPerMM=$DotsPerMM \
    -o ${ImageName}-press.stl Cookie\ Cutter.scad
    

    The OpenSCAD program are unchanged from the cookie cutter process.

  • Sony NP-BX1 Battery Test Fixture

    The Sony HDR-AS30V “action camera” uses NP-BX1 lithium batteries (3.7 V @ 1.24 A·h = 4.6 W·h) that are, of course, a completely different size and shape than any other lithium battery on the planet.

    So.

    Tweaking a few dimensions in the Canon NB-6L source code, tinkering with the layout of the contact pins, and shazam Yet Another 3D Printed Battery Test Fixture:

    NP-BX1 Holder - show layout
    NP-BX1 Holder – show layout

    It builds nicely, although the contact pin tunnels are a bit too close to the top of the case:

    Sony NP-BX1 Holder - on platform
    Sony NP-BX1 Holder – on platform

    After reaming out the contact pin holes to the proper diameters & depths, then gluing the plugs in place, it works just as you’d expect:

    Sony NP-BX1 battery holder
    Sony NP-BX1 battery holder

    It’s worth noting that the Wasabi charger accepts the batteries upside-down, with the conspicuous chevron against the charger body. It’s definitely not the way all the other chargers work. The keying recesses on the battery (corresponding to the blocks in the solid model) lie along the bottom edge of the contact surface, so flipping the battery over means they’ll hold it in place, but … oh, well.

    That grotty Powerpole connector last saw use in some random benchtop lashup. At some point I’ll be forced to start making more of those.

    The OpenSCAD source code:

    // Holder for Sony NP-BX1 Li-Ion battery
    // Ed Nisley KE4ZNU January 2013
    
    include <MCAD/boxes.scad>
    
    // Layout options
    
    Layout = "Show";					//  Show Build Fit Case Lid Pins Plugs AlignPins
    
    //- Extrusion parameters - must match reality!
    //  Print with +2 shells and 3 solid layers
    
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    
    HoleWindage = 0.2;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    Protrusion = 0.1;			// make holes end cleanly
    
    inch = 25.4;
    
    BuildOffset = 3.0;			// clearance for build layout
    
    Gap = 8.0;					// separation for Fit parts
    
    //- Battery dimensions - rationalized from several samples
    //  Coordinate origin at battery corner by contact plates on bottom surface
    
    BatteryLength = 43.0;
    BatteryWidth = 30.0;
    BatteryThick =  9.5;
    
    ContactWidth = 2.90;
    ContactLength = 4.30;
    ContactRecess = 0.90;
    
    ContactOC = 10.0;			// center-to-center across contact face
    ContactOffset = 6.20;		// offset from battery edge
    ContactHeight = 6.30;		// offset from battery bottom plane
    
    AlignThick = 2.75;			// alignment recesses on contact face
    AlignDepth = 1.70;			// into face
    AlignWidth1 = 3.70;			// across face at contacts
    AlignWidth2 = 3.60;			//  ... other edge
    
    //- Pin dimensions
    
    PinTipDia = 1.6;
    PinTipLength = 10.0;
    
    PinTaperLength = 2.3;
    
    PinShaftDia = 2.4;
    PinShaftLength = 6.8;
    
    PinFerruleDia = 3.1;
    PinFerruleLength = 2.0;
    
    PinLength = PinTipLength + PinTaperLength + PinShaftLength + PinFerruleLength;
    
    ExtendRelax = 1.5 + ContactRecess;		// pin extension when no battery is present
    ExtendOvertravel = 1.0;					//  ... beyond engaged position
    
    //- Spring dimensions
    
    SpringDia = 3.1;						// coil OD
    SpringMax = 9.3;
    SpringLength = SpringMax - 0.5;			// slightly compressed
    SpringMin = 4.5;
    
    SpringPlugOD = IntegerMultiple(5.0,ThreadWidth);		// plug retaining the spring
    SpringPlugID = 2.0;
    SpringPlugLength = IntegerMultiple(4.0,ThreadWidth);
    SpringPlugSides = 3*4;
    
    SpringTravel = ExtendRelax + ExtendOvertravel;
    
    //- Holder dimensions
    
    GuideRadius = ThreadWidth;			// friction fit ridges
    GuideOffset = 7;					// from compartment corners
    WallThick = 4*ThreadWidth;			// holder sidewalls
    
    BaseThick = 6*ThreadThick;			// bottom of holder to bottom of battery
    TopThick = 6*ThreadThick;			// top of battery to top of holder
    
    ThumbRadius = 10.0;			// thumb opening at end of battery
    
    CornerRadius = 3*ThreadThick;			// nice corner rounding
    
    CaseLength = SpringPlugLength + SpringLength + PinLength - ExtendRelax
    			+ BatteryLength + GuideRadius + WallThick;
    CaseWidth = 2*WallThick + 2*GuideRadius + BatteryWidth;
    CaseThick = BaseThick + BatteryThick + TopThick;
    
    AlignPinOD = 1.75;			// lid alignment pins - filament snippets
    AlignPinLength = 5.0;
    AlignPinInset = 7.0;
    AlignPinOffset = -3.75;		//  from centerline - choose to miss contact pins
    
    //- XY origin at front left battery corner, Z on platform below that
    
    CaseLengthOffset = -(SpringPlugLength + SpringLength + PinLength - ExtendRelax);
    CaseWidthOffset = -(WallThick + GuideRadius);
    CaseThickOffset = BaseThick;
    
    LidLength = ExtendRelax - CaseLengthOffset;
    
    echo(str("Contact pin tip dia: ",PinTipDia));
    echo(str("Drill depth to taper end: ",
    		 (SpringPlugLength + SpringLength + PinFerruleLength + PinShaftLength + PinTaperLength),
    		 " -- Dia: ",PinShaftDia));
    echo(str("            to ferrule end: ",
    		  (SpringPlugLength + SpringLength + PinFerruleLength),
    		 " -- Dia: ",PinFerruleDia));
    echo(str("            to plug end: ",SpringPlugLength,
    		 " -- Dia: ",SpringPlugOD));
    
    //----------------------
    // Useful routines
    
    module PolyCyl(Dia,Height,ForceSides=0) {			// based on nophead's polyholes
    
      Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    
      FixDia = Dia / cos(180/Sides);
    
      cylinder(r=(FixDia + HoleWindage)/2,
               h=Height,
    	   $fn=Sides);
    }
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      Range = floor(50 / Space);
    
    	for (x=[-Range:Range])
    	  for (y=[-Range:Range])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    //-------------------
    
    //-- Guides for tighter friction fit
    
    module Guides() {
      	  translate([GuideOffset,-GuideRadius,CaseThickOffset])
    		PolyCyl(2*GuideRadius,(BatteryThick - Protrusion),4);
    	  translate([GuideOffset,(BatteryWidth + GuideRadius),CaseThickOffset])
    		PolyCyl(2*GuideRadius,(BatteryThick - Protrusion),4);
    	  translate([(BatteryLength - GuideOffset),-GuideRadius,CaseThickOffset])
    		PolyCyl(2*GuideRadius,(BatteryThick - Protrusion),4);
    	  translate([(BatteryLength - GuideOffset),(BatteryWidth + GuideRadius),CaseThickOffset])
    		PolyCyl(2*GuideRadius,(BatteryThick - Protrusion),4);
    	  translate([(BatteryLength + GuideRadius),GuideOffset/2,CaseThickOffset])
    		PolyCyl(2*GuideRadius,(BatteryThick - Protrusion),4);
    	  translate([(BatteryLength + GuideRadius),(BatteryWidth - GuideOffset/2),CaseThickOffset])
    		PolyCyl(2*GuideRadius,(BatteryThick - Protrusion),4);
    
    }
    
    //-- Contact pins (holes therefore)
    
    module PinShape() {
    
      union() {
    	cylinder(r=(PinTipDia + HoleWindage)/2,h=(PinTipLength + Protrusion),$fn=6);
    
    	translate([0,0,PinTipLength])
    	  cylinder(r=(PinShaftDia + HoleWindage)/2,
    			   h=(PinTaperLength + PinShaftLength + Protrusion),$fn=6);
    
    	translate([0,0,(PinLength - PinFerruleLength)])
    	  cylinder(r=(PinFerruleDia + HoleWindage)/2,
    				h=(PinFerruleLength + Protrusion),$fn=6);
    
    	translate([0,0,(PinLength)])
    	  cylinder(r=(SpringDia + HoleWindage)/2,
    				h=(SpringLength + Protrusion),$fn=6);
    
    	translate([0,0,(PinLength + SpringLength - HoleWindage)])	// windage for hole length
    	  cylinder(r=(SpringPlugOD + HoleWindage)/2,h=3*SpringPlugLength,$fn=SpringPlugSides);
    
    //	  translate([0,0,(PinLength + SpringLength + SpringPlugLength)])
    //	  cylinder(r=(SpringPlugOD + HoleWindage)/2,h=2*SpringPlugLength,$fn=SpringPlugSides);	// extend hole
      }
    
    }
    
    module PinAssembly() {
    
      translate([ExtendRelax,ContactOffset,CaseThickOffset + ContactHeight]) {
    	rotate([0,270,0]) {
    	  PinShape();												// pins
    	  translate([0,(1*ContactOC),0])
    		PinShape();
    	}
      }
    
    }
    
    //-- Alignment pins
    
    module AlignPins() {
    
    	for (x=[-1,1])
    		translate([x*(LidLength - 2*AlignPinInset)/2,AlignPinOffset,0])
    			rotate(45)
    			PolyCyl(AlignPinOD,AlignPinLength);
    }
    
    //-- Case with origin at battery corner
    
    module Case() {
    
      difference() {
    
    	union() {
    
    	  difference() {
    		translate([(CaseLength/2 + CaseLengthOffset),
    				  (CaseWidth/2 + CaseWidthOffset),
    				  (CaseThick/2)])
    		  roundedBox([CaseLength,CaseWidth,CaseThick],CornerRadius); 	// basic case shape
    
    		translate([-ExtendOvertravel,-GuideRadius,CaseThickOffset])
    		  cube([(BatteryLength + GuideRadius + ExtendOvertravel),
    				(BatteryWidth + 2* GuideRadius),
    				(BatteryThick + Protrusion)]);						// battery space
    
    	  }
    
    	  Guides();
    
    	  translate([-ExtendOvertravel,-GuideRadius,BaseThick])
    		cube([(AlignDepth + ExtendOvertravel),
    			  (AlignWidth1 + GuideRadius),
    			  AlignThick]);											// alignment blocks
    	  translate([-ExtendOvertravel,
    				 (BatteryWidth - AlignWidth2),
    				 BaseThick])
    		cube([(AlignDepth + ExtendOvertravel),
    			  (AlignWidth2 + GuideRadius),
    			  AlignThick]);
    
    	}
    
    	translate([(-ExtendOvertravel),
    			   (CaseWidthOffset - Protrusion),
    			   (CaseThickOffset + BatteryThick)])
    	  cube([CaseLength,
    		    (CaseWidth + 2*Protrusion),
    		    (TopThick + Protrusion)]);								// battery access
    
    	translate([(CaseLengthOffset - Protrusion),
    			   (CaseWidthOffset - Protrusion),
    			   (CaseThickOffset + BatteryThick)])
    	  cube([(CaseLength + 2*Protrusion),
    		    (CaseWidth + 2*Protrusion),
    		    (TopThick + Protrusion)]);								// battery insertion allowance
    
    	translate([(BatteryLength - Protrusion),
    			    (CaseWidth/2 + CaseWidthOffset),
    			    (CaseThickOffset + ThumbRadius)])
    	  rotate([90,0,0])
    		rotate([0,90,0])
    		  cylinder(r=ThumbRadius,
    				   h=(WallThick + GuideRadius + 2*Protrusion),
    				   $fn=22);											// remove thumb notch
    
    	PinAssembly();
    
    	translate([-LidLength/2,BatteryWidth/2,CaseThick - TopThick - (AlignPinLength - TopThick/2)])
    		AlignPins();
      }
    
    }
    
    module Lid() {
    
      difference() {
    	translate([0,0,(CaseThick/2 - BaseThick - BatteryThick)])
    	  roundedBox([LidLength,
    				 CaseWidth,CaseThick],CornerRadius);
    
    	translate([0,0,-(CaseThick/2)])
    	  cube([(LidLength + 2*Protrusion),
    		    (CaseWidth + 2*Protrusion),
    		    (CaseThick)],center=true);
    
    	translate([-ExtendRelax,0,-(AlignPinLength - TopThick/2)])
    		AlignPins();
      }
    
    }
    
    module PlugShape() {
    
      difference() {
    	cylinder(r=SpringPlugOD/2,h=SpringPlugLength,$fn=SpringPlugSides);
    	translate([0,0,-Protrusion])
    	  PolyCyl(SpringPlugID,(SpringPlugLength + 2*Protrusion),SpringPlugSides);
      }
    }
    
    module Plugs() {
      translate([0,ContactOC,0])
    	PlugShape();
      translate([0,-ContactOC,0])
    	PlugShape();
    }
    
    //-------------------
    // Build it!
    
    ShowPegGrid();
    
    if (Layout == "Case")
      Case();
    
    if (Layout == "Lid")
      Lid();
    
    if (Layout == "Plugs")
    	for (i=[-1:1])
    		translate([i*1.5*SpringPlugOD,0,0])
    			Plugs();
    
    if (Layout == "Pins")
      PinShape();
    
    if (Layout == "AlignPins")
      AlignPins();
    
    if (Layout == "Show") {								// reveal pin assembly
      difference() {
    	Case();
    
    	translate([(CaseLengthOffset - Protrusion),
    			   (CaseWidthOffset - Protrusion + WallThick + ContactOffset + ContactOC),
    			   (BaseThick + ContactHeight)])
    	  cube([(-CaseLengthOffset + Protrusion),
    			 (CaseWidth + 2*Protrusion),
    			 CaseThick + BaseThick - ContactHeight + Protrusion]);
    
    	translate([(CaseLengthOffset - Protrusion),
    			   (CaseWidthOffset - Protrusion),
    			   -Protrusion])
    	  cube([(-CaseLengthOffset + Protrusion),
    			 (WallThick + GuideRadius + ContactOffset + Protrusion),
    			 CaseThick]);
      }
    
      translate([ExtendRelax,ContactOffset,(CaseThickOffset + ContactHeight)]) {	// pins
    	rotate([0,270,0]) {
    	  %PinShape();
    //	  translate([0,(2*ContactOC),0])
    //		%PinShape();
    	}
      }
    
      translate([CaseLengthOffset,ContactOffset,(CaseThickOffset + ContactHeight)])
    	rotate([0,90,0])
    	  PlugShape();
    }
    
    if (Layout == "Build") {
      translate([-(CaseLength/2 + CaseLengthOffset),-(CaseWidthOffset - BuildOffset),0])
    	Case();
      translate([CaseWidth/2,(CaseLengthOffset/2 - BuildOffset),0])
    	rotate([0,0,90])
    	  Lid();
      for (i=[-1:1])
    	translate([CaseLengthOffset/2 + i*1.5*SpringPlugOD,-CaseWidth/2,0])
    		Plugs();
    }
    
    if (Layout == "Fit") {
      Case();
      translate([(-LidLength/2 + ExtendRelax),
    			(CaseWidth/2 + CaseWidthOffset),
    			(BaseThick + BatteryThick + Gap)])
    	  Lid();
      translate([ExtendRelax,ContactOffset,CaseThickOffset + ContactHeight]) {	// pins
    	rotate([0,270,0]) {
    	  %PinShape();
    	  translate([0,(1*ContactOC),0])
    		%PinShape();
    	}
      }
    
      translate([CaseLengthOffset,
    			(ContactOffset + ContactOC),
    			(CaseThickOffset + ContactHeight)])
      rotate([0,90,0])
    	Plugs();
    
      translate([-LidLength/2,BatteryWidth/2,CaseThick])
    #	AlignPins();
    
    }
    
  • Quilting Pin Caps: More!

    These quilting pin caps are slightly longer than the previous version and, due to the M2’s smaller nozzle, have slightly thinner single-thread walls. Because Slic3r does a better (although not ideal) job of path planning than Skeinforge, it’s easier to create an array of the caps in the solid model than to manually add duplicates in Slic3r:

    Quilting Pin Cap - array
    Quilting Pin Cap – array

    They look like egg cases from Prometheus:

    Quilting pin cap array - on platform
    Quilting pin cap array – on platform

    Fill with silicone caulk on waxed paper and they look even more like that:

    Quilting pin caps - silicone fill
    Quilting pin caps – silicone fill

    Fast-forward a few days, rub off the excess caulk, trim off a few blobs, and they’re ready for presentation:

    Quilting pin caps - finished
    Quilting pin caps – finished

    In use, they look about like you’d expect:

    Quilting pin caps - in use
    Quilting pin caps – in use

    The pin caps I made from a 5 gallon bucket’s O-ring gasket didn’t work out well, as the plastic didn’t like being poked with pins and put up a stiff resistance. Silicone caulk has exactly the right consistency.

    When Mary ramps up a full-scale quilt, we’ll need a few hundred of the things. The commercial version has dropped to 40 cents each, which makes all this worthwhile.

    The OpenSCAD source code:

    // Quilting pin caps
    // Ed Nisley KE4ZNU April 2012
    //	January 2013 - modify for Slic3r and M2
    
    //- Extrusion parameters must match reality!
    //  Print with +1 shells and 3 solid layers
    
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    
    HoleWindage = 0.2;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    Protrusion = 0.1;			// make holes end cleanly
    
    //----------------------
    // Dimensions
    
    ID = 5.0;
    OD = ID + 2*ThreadWidth;
    Length = 8.0;
    Sides = 8;
    
    CapArray = [6,6];			// XY layout of caps
    CapsOC = OD + 2.0;			// OC spacing
    
    //----------------------
    // Useful routines
    
    module PolyCyl(Dia,Height,ForceSides=0) {			// based on nophead's polyholes
    
      Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    
      FixDia = Dia / cos(180/Sides);
    
      cylinder(r=(FixDia + HoleWindage)/2,
               h=Height,
    	   $fn=Sides);
    }
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      RangeX = floor(100 / Space);
      RangeY = floor(125 / Space);
    
    	for (x=[-RangeX:RangeX])
    	  for (y=[-RangeY:RangeY])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    module PinCap() {
    	rotate(180/Sides) {
    		difference() {
    		PolyCyl(OD,Length,8);
    		translate([0,0,-Protrusion])
    			PolyCyl(ID,(Length + 2*Protrusion),8);
    		}
    	}
    
    }
    
    //----------------------
    // Build them!
    
    ShowPegGrid();
    
    translate([(-CapsOC*(CapArray[0] - 1)/2),(-CapsOC*(CapArray[1] - 1)/2),0])
    	for (i=[0:(CapArray[0] - 1)],j=[0:(CapArray[1] - 1)])
    		translate([i*CapsOC,j*CapsOC,0])
    			PinCap();
    

    They seem to work pretty well…

  • Modified Quilting Foot: Speed Wrench Knob

    The Nyloc nut atop that modified quilting foot requires more grip than fingers can provide:

    Modified Darning Foot - in action
    Modified Darning Foot – in action

    The “precision” wrench I adapted to that nut works for small adjustments, but for larger ones it’s easier to take the foot off and spin this knob:

    Quilting Foot Knob - knurling
    Quilting Foot Knob – knurling

    It has a hex opening in each end that fits the nut, with a through hole for the bolt. The top looks exactly like you’d expect:

    Quilting Foot Knob - top
    Quilting Foot Knob – top

    The bottom needs a bit of support:

    Quilting Foot Knob - bottom support
    Quilting Foot Knob – bottom support

    The solid model shows off the support in color:

    Quilting Foot Knob
    Quilting Foot Knob

    The OpenSCAD source code doesn’t have many surprises:

    // Quilting foot knob
    // Ed Nisley KE4ZNU January 2013
    
    use <knurledFinishLib_v2.scad>
    
    //- Extrusion parameters must match reality!
    //  Print with +1 shells and 3 solid layers
    
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    
    HoleWindage = 0.2;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    Protrusion = 0.1;			// make holes end cleanly
    
    //----------------------
    // Dimensions
    
    KnobOD = 20.0;
    KnobLength = 25.0;
    KnobSides = 12;
    
    DiamondLength = KnobLength/3;
    DiamondWidth = DiamondLength/2;
    DiamondDepth = 1.0;
    
    NutOD = 7.0;				// across flats!
    NutLength = 6.0;
    ScrewOD = 4.0;
    
    DoSupport = true;
    
    //----------------------
    // Useful routines
    
    module PolyCyl(Dia,Height,ForceSides=0) {			// based on nophead's polyholes
    
      Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    
      FixDia = Dia / cos(180/Sides);
    
      cylinder(r=(FixDia + HoleWindage)/2,
               h=Height,
    	   $fn=Sides);
    }
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      RangeX = floor(100 / Space);
      RangeY = floor(125 / Space);
    
    	for (x=[-RangeX:RangeX])
    	  for (y=[-RangeY:RangeY])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    module Knob() {
    	rotate(180/Sides) {
    		difference() {
    //			cylinder(r=KnobOD/2,h=KnobLength,$fn=KnobSides);
    			render(convexity=10)
    			knurl(k_cyl_hg=KnobLength,
    				  k_cyl_od=KnobOD,
    				  knurl_wd=DiamondWidth,
    				  knurl_hg=DiamondLength,
    				  knurl_dp=DiamondDepth,
    				  e_smooth=DiamondLength/2);
    			translate([0,0,-Protrusion])
    				PolyCyl(ScrewOD,(KnobLength + 2*Protrusion),6);
    			translate([0,0,(KnobLength - NutLength)])
    				PolyCyl(NutOD,(NutLength + Protrusion),6);
    			translate([0,0,-Protrusion])
    				PolyCyl(NutOD,(NutLength + Protrusion),6);
    		}
    	}
    }
    
    module Support() {
    	color("Yellow")
    		for (Seg=[0:5]) {
    			rotate(360*Seg/6)
    			translate([0,0,(NutLength - ThreadThick)/2])
    				cube([(NutOD - 1*ThreadWidth),
    						2*ThreadWidth,
    						(NutLength - ThreadThick)],
    						center=true);
    			}
    }
    
    //----------------------
    // Build them!
    
    ShowPegGrid();
    
    Knob();
    
    if (DoSupport)
    	Support();
    

    Mary likes it… and thinks I’m being silly. She’s right, of course.