I loves me some good error message:

A closer look:

If the popup appeared just one pixel lower, you could easily decode the message behind it; perhaps * Out of Service * fits?
At least it doesn’t show an OK? button.
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.
General-purpose computers doing something specific
Given an STL file generated from a height map image, import it into OpenSCAD:

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

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:

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:

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:

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

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();
}
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:
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:

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:

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.
The avconv (formerly ffmpeg) image-to-video programs expect sequentially numbered files, with the numbers in a fixed-width part of the file name, thusly: dsc00001.jpg.
Given a set of files (previously normalized to lowercase) like this:
ll | head total 286576 -rwxr-xr-x 1 ed ed 595708 Jan 23 19:14 dsc00940.jpg -rwxr-xr-x 1 ed ed 515561 Jan 23 19:14 dsc00941.jpg -rwxr-xr-x 1 ed ed 580190 Jan 23 19:14 dsc00942.jpg -rwxr-xr-x 1 ed ed 571387 Jan 23 19:14 dsc00943.jpg -rwxr-xr-x 1 ed ed 573207 Jan 23 19:14 dsc00944.jpg -rwxr-xr-x 1 ed ed 571086 Jan 23 19:14 dsc00945.jpg -rwxr-xr-x 1 ed ed 571600 Jan 23 19:14 dsc00946.jpg -rwxr-xr-x 1 ed ed 571547 Jan 23 19:14 dsc00947.jpg -rwxr-xr-x 1 ed ed 565706 Jan 23 19:15 dsc00948.jpg
A Bash one-liner loop does the renumbering:
sn=1 ; for f in *jpg ; do printf -v dn 'dsc%05d.jpg' "$(( sn++ ))" ; mv $f $dn ; done
The results look pretty much like you’d expect:
ll | head total 286556 -rwxr-xr-x 1 ed ed 595708 Jan 23 19:14 dsc00001.jpg -rwxr-xr-x 1 ed ed 515561 Jan 23 19:14 dsc00002.jpg -rwxr-xr-x 1 ed ed 580190 Jan 23 19:14 dsc00003.jpg -rwxr-xr-x 1 ed ed 571387 Jan 23 19:14 dsc00004.jpg -rwxr-xr-x 1 ed ed 573207 Jan 23 19:14 dsc00005.jpg -rwxr-xr-x 1 ed ed 571086 Jan 23 19:14 dsc00006.jpg -rwxr-xr-x 1 ed ed 571600 Jan 23 19:14 dsc00007.jpg -rwxr-xr-x 1 ed ed 571547 Jan 23 19:14 dsc00008.jpg -rwxr-xr-x 1 ed ed 565706 Jan 23 19:15 dsc00009.jpg
Because you’re renaming the files anyway, don’t bother to normalize ’em:
sn=1 ; for f in *JPG ; do printf -v dn 'dsc%05d.jpg' "$(( sn++ ))" ; mv $f $dn ; done
And, of course, you can fetch ’em from the camera while doing that:
sn=1 ; for f in /mnt/part/DCIM/100MSDCF/*JPG ; do printf -v dn 'dsc%05d.jpg' "$(( sn++ ))" ; cp -a $f $dn ; done
That leaves the DSC*JPG original files on the camera, where you can delete all of them in one operation when you’re happy with the results.
If you don’t need the full resolution, reserialize and resize each picture on the fly:
sn=1 ; for f in /mnt/part/DCIM/100MSDCF/*JPG ; do printf -v dn 'dsc%05d.jpg' "$(( sn++ ))" ; convert $f -resize 50% $dn ; done
That’s based on combining several hints turned up by the usual Google search.
To assemble a quick-and-simple movie from the images:
avconv -r 30 -i dsc%05d.jpg -q 5 movie.mp4
The image quality certainly isn’t up to what you (well, I) would expect from a 1920×1080 “HD” file, but the Sony HDR-AS30V Zeiss camera lens seems to be a fisheye pinhole (170° view angle, 2.5 mm f/2.8) backed with relentless image compression:

Memo to Self: It’s not worth creating and remembering Yet Another Script.
While converting a stop-action series of images from the HDR-AS30V into a movie, I wanted change all the image files on a USB Flash drive from DSC00008.JPG to dsc00008.jpg, so as to simplify typing their names.
Alas, because the camera’s exFAT filesystem cares not one whit about case, the obvious command doesn’t work:
rename 's/JPG/jpg/' /mnt/part/* /mnt/part/DSC00008.JPG not renamed: /mnt/part/DSC00008.jpg already exists
So you must do each piece in two steps:
rename 's/JPG/jpgx/' /mnt/part/* rename 's/jpgx/jpg/' /mnt/part/* rename 's/DSC/dscx/' /mnt/part/* rename 's/dscx/dsc/' /mnt/part/*
Obvious once you see it, I suppose…
See the comments for a better way:
rename 'y/A-Z/a-z/' *JPG
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:

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

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:

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();
}
The Nyloc nut atop that modified quilting foot requires more grip than fingers can provide:

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:

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:

The bottom needs a bit of support:

The solid model shows off the support in color:

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.