Posts Tagged CNC

Thing-O-Matic Y-axis Idler Support Bracket: Oops

The STL file from CampbellsBot’s Y-Axis Idler Support Bracket printed without incident (admittedly, on the M2):

Thing-O-Matic Y-axis Idler Support Bracket

Thing-O-Matic Y-axis Idler Support Bracket

Come to find out that Makerbot changed the spacing between the Y-axis rod and the idler bolt, so it doesn’t fit the TOM286. I could fire up the Token Windows Box, install Sketchup, modify the model, rebuild and clean up the STL, and try again, but it’s easier to just give up. The TOM286 has worked fine so far, so maybe this isn’t really needed.

Ah, well, it’s another show-n-tell doodad…

About these ads

, ,

4 Comments

Can Opener Gear Rebuild

Cleaning up the wrecked gears on the can opener made it painfully obvious that I had to conjure at least one gear to get the poor thing working again:

Can opener - gears and cutters

Can opener – gears and cutters

Fortunately, those are more in the line of cogs, rather than real gears, so I decided a crude hack would suffice: drill a pattern of holes to define the openings between the teeth, file / grind the teeth reasonably smooth, and then tweak the shape to suit.

Fitting some small number-size drills between the remains of the teeth showed:

  • A #52 = 52.0 mil = 1.32 mm drill matched the root curvature
  • A #28 = 140.5 mil = 3.57 mm drill was tangent to the small drill and the tooth walls

Neither of those count as precision measurements, particularly given the ruined teeth, but they’re close enough for a first pass.

The OEM drive gear (on the right) has the teeth bent upward to mate with the cutter gear (on the left), but under normal gripping force, the teeth don’t mesh securely and tend to slide over / under / past each other. However, if I were to cut the drive gear from a metal sheet that’s thick enough to engage both the root and the crest of the cutter gear, that should prevent all the slipping & sliding. Some eyeballometric guesstimation suggested 2.5 mm would be about right and the Basement Laboratory Stockpile produced a small slab of 100 mil = 2.54 mm aluminum sheet.

However, the center part of the gear must have the same thickness as the OEM gear to keep the drive wheel at the same position relative to the cutter blade, which means a bit of pocket milling. I have some small ball burrs that seemed like they might come in handy.

A recent thread on the LinuxCNC mailing list announced Bertho Stultien’s gcmc, the G-Code Meta Compiler, and this looked like a golden opportunity to try it out. Basically, gcmc lets you write G-Code programs in a C-like language that eliminates nearly all the horrendous syntactic noise of raw G-Code. I like it a lot and you’ll be seeing more of it around here…

The gcmc source code, down below, include a function that handles automatic tool height probing, using that simple white-goods switch. The literal() function emits whatever you hand it as text for the G-Code file, which is how you mechanize esoteric commands that gcmc doesn’t include in its repertoire. It’s basically the same as my bare G-Code probe routine, but now maintains a state variable that eliminates the need for separate first-probe and subsequent-probe entry points.

One point that tripped me up, even though I should know better: because gcmc is a compiler, it can’t read G-Code parameters that exist only when LinuxCNC (or whatever) is interpreting the G-Code. You can write parameters with values computed at compile time, but you can’t read and process them in the gcmc program.

Anyhow, the first pass produced an array of holes that, as I fully expected, weren’t quite right:

Can opener gear - first hole pattern

Can opener gear – first hole pattern

The second pass got the root and middle holes tangent to each other:

Can opener gear - second hole pattern

Can opener gear – second hole pattern

It also ran a center drill pass for those tiny little holes to prevent their drill from wandering about. The other drills are about the same size as the center drill, so they’re on their own.

The rosette around the central hole comes from sweeping the burr in a dozen overlapping circles tangent to the outer diameter, then making a cleanup pass around the OD:

Can opener gear - 12 leaf rosette

Can opener gear – 12 leaf rosette

Incidentally, that stray hole between the two patterns came from the aluminum sheet’s previous life, whatever it may have been. There are three other holes, two of which had flat washers taped to them, so your guess is as good as mine. That’s my story and I’m sticking with it.

Introducing the sheet to Mr Bandsaw and cutting through the outer ring produced a bizarre snowflake:

Can opener gear - cut out

Can opener gear – cut out

Cutting off the outer ring of holes turned the incipient gear body into a ragged shuriken:

Can opener gear - isolated

Can opener gear – isolated

A few minutes of increasingly deft Dremel cutoff wheel work, poised on the bench vise over the shopvac nozzle to capture the dust, produced a credible gear shape:

Can opener gear - first pass

Can opener gear – first pass

Iterating through some trial fits, re-grinds, and general fiddling showed that the center pocket was too shallow. The cutter wheel should slightly clear the drive wheel, but it’s an interference fit:

Can opener gear - trial fit

Can opener gear – trial fit

Which, of course, meant that I had to clamp the [mumble] thing back in the Sherline and re-mill the pocket. The trick is to impale it on the wrong end of a suitable drill, clamp it down, and touch off that spot as the origin:

Can opener gear - re-centering

Can opener gear – re-centering

I took the opportunity to switch to a smaller ball and make 16 little circles to clear the pocket:

Can Opener Gear - 16 leaf rosette

Can Opener Gear – 16 leaf rosette

Now that’s better:

Can opener gear - deeper pocket

Can opener gear – deeper pocket

Another trial fit showed that everything ended up in the right place:

Can opener gear - final fit

Can opener gear – final fit

I gave it a few cranks, touched up any cogs that clashed with the (still misshapen) cutter gear, applied it to a randomly chosen can, and it worked perfectly:

  • Squeeze the levers to easily punch through the lid
  • Crankety crank on the handle, while experiencing none of the previous drama
  • The severed lid falls into the can

Which is exactly how it’s supposed to work. What’s so hard about that?

What you can’t see in that picture is the crest of the lowest cutter gear tooth fitting just above the bottom of the drive gear root. Similarly, the crest of the highest drive gear tooth remains slightly above the cutter root. That means the cutter gear teeth always engage the drive gear, there’s no slipping & sliding, and it’s all good.

Aluminum isn’t the right material for a gear-like object meshed with a steel counterpart, but it’s easy to machine on a Sherline. I’ll run off a few more for show-n-tell and, if when this one fails, I’ll have backup.

The gcmc source code:

// Can opener drive gears
//	Ed Nisley KE4ZNU - February 2014
//	Sherline CNC mill with tool height probe
//	XYZ touchoff origin at center on fixture surface

DO_DRILLCENTER	= 1;
DO_MILLCENTER	= 1;
DO_DRILLINNER	= 1;
DO_DRILLOUTER	= 1;
DO_DRILLTIPS	= 1;

//----------
// Overall dimensions

GearThick = 2.54;			// overall gear thickness
GearCenterThick = 1.75;		// thickness of gear center

GearTeeth = 12;				// number of teeth!
ToothAngle = 360deg/GearTeeth;
GearOD = 22.0;				// tooth tip
GearID = 13.25;				// tooth root

SafeZ = 20.0;				// guaranteed to clear clamps
TravelZ = GearThick + 1.0;	// guaranteed to clear plate

//----------
// Tool height probe
//	Sets G43.1 tool offset in G-Code, so our Z=0 coordinate always indicates the touchoff position

ProbeInit = 0;					// 0 = not initialized, 1 = initialized
ProbeSpeed = 400.0mm;
ProbeRetract = 1.0mm;

PROBE_STAY = 0;					// remain at probe station
PROBE_RESTORE = 1;				// return to previous location after probe

function ProbeTool(RestorePos) {

local WhereWasI;

	WhereWasI = position();

	if (ProbeInit == 0) {		// probe with existing tool to set Z=0 as touched off
		ProbeInit++;
		literal("#<_Probe_Speed> = ",to_none(ProbeSpeed),"\n");
		literal("#<_Probe_Retract> = ",to_none(ProbeRetract),"\n");
		literal("#<_ToolRefZ> = 0.0 \t; prepare for first probe\n");
		ProbeTool(PROBE_STAY);
		literal("#<_ToolRefZ> = #5063 \t; save touchoff probe point\n");
		literal("G43.1 Z0.0 \t; set zero offset = initial touchoff\n");
	}
	elif (ProbeInit == 1) {		// probe with new tool, adjust offset accordingly
		literal("G49 \t; clear tool length comp\n");
		literal("G30 \t; move over probe switch\n");
		literal("G59.3 \t; use coord system 9\n");
		literal("G38.2 Z0 F#<_Probe_Speed> \t; trip switch on the way down\n");
		literal("G0 Z[#5063 + #<_Probe_Retract>] \t; back off the switch\n");
		literal("G38.2 Z0 F[#<_Probe_Speed> / 10] \t; trip switch slowly\n");
		literal("#<_ToolZ> = #5063 \t; save new tool length\n");
		literal("G43.1 Z[#<_ToolZ> - #<_ToolRefZ>] \t; set new length\n");
		literal("G54 \t; return to coord system 0\n");
		literal("G30 \t; return to safe level\n");
	}
	else {
		error("*** ProbeTool sees invalid ProbeInit: ",ProbeInit);
		comment("debug,*** ProbeTool sees invalid ProbeInit: ",ProbeInit);
		ProbeInit = 0;
	}

	if (RestorePos == PROBE_RESTORE) {
		goto(WhereWasI);
	}

}

//----------
// Utility functions

function WaitForContinue(MsgStr) {
	comment(MsgStr);
	pause();
}

function CueToolChange(MsgStr) {
	literal("G0 Z" + SafeZ + "\n");
	literal("G30\n");
	WaitForContinue(MsgStr);
}

function ToolChange(Info,Name) {
	CueToolChange("msg,Insert " + to_mm(Info[TOOL_DIA]) + " = " + to_in(Info[TOOL_DIA]) + " " + Name);
	ProbeTool(PROBE_STAY);

	WaitForContinue("msg,Set spindle to " + Info[TOOL_SPEED] + " rpm");
	feedrate(Info[TOOL_FEED]);
}

function GetAir() {
	goto([-,-,SafeZ]);
}

//-- compute drill speeds & feeds based on diameter
//		rule of thumb is 100 x diameter at 3000 rpm for real milling machines
//		my little Sherline's Z axis can't produce enough thrust for that!

MaxZFeed = 600.0mm;				// fastest possible Z feed

TOOL_DIA = 0;					// Indexes into DrillParam() result
TOOL_SPEED = 1;					//  spindle RPM
TOOL_FEED = 2;					//	linear feed
TOOL_TIP = 3;					//	length of 118 degreee drill tip

function DrillParam(Dia) {
local RPM,Feed,Tip,Data,Derating;

	Derating = 0.25;			// derate from (100 x diameter) max feed

	RPM = 3000.0;				// default 3 k rpm

	Feed = Derating * (100.0 * Dia);
	if (Feed > MaxZFeed) {
		RPM *= (MaxZFeed / Feed);	//  scale speed downward to fit
		Feed = MaxZFeed;
	}

	Tip = (Dia/2) * tan(90deg - 118deg/2);
	Data = [Dia,RPM,Feed,Tip];

	message("DrillParam: ",Data);
	return Data;
}

//-- peck drilling cycle

function PeckDrill(Endpt,Retract,Peck) {
	literal("G83 X",to_none(Endpt[0])," Y",to_none(Endpt[1])," Z",to_none(Endpt[2]),
			" R",to_none(Retract)," Q",to_none(Peck),"\n");
}

//----------
// Make it happen

literal("G99\t;  retract to R level, not previous Z\n");

WaitForContinue("msg,Verify: G30 position in G54 above tool change switch?");

WaitForContinue("msg,Verify: fixture origin XY touched off at center of gear?");

WaitForContinue("msg,Verify: Z touched off on top surface at " + GearThick + "?");
ProbeTool(PROBE_STAY);

//-- Drill center hole

if (DO_DRILLCENTER) {

	DrillData = DrillParam(5.0mm);
	ToolChange(DrillData,"drill");

	goto([0,0,-]);
	goto([-,-,TravelZ]);

	drill([0,0,-1.5*DrillData[TOOL_TIP]],TravelZ,DrillData[TOOL_DIA]);
	GetAir();

}

//-- Drill inner ring

if (DO_DRILLINNER) {

	DrillData = DrillParam(1.32mm);

	RingRadius = GearID/2.0 + DrillData[TOOL_DIA]/2.0;		// center of inner ring holes
	HolePosition = [RingRadius,0mm,-1.5*DrillData[TOOL_TIP]];

//	but first, center-drill to prevent drifting

	CDData = DrillParam(1.00mm);			// pretend it's a little drill
	CDData[TOOL_FEED] = 100mm;				//  ... use faster feed

	CDPosition = HolePosition;				// use center drill coordinates
	CDPosition[2] = GearThick - 0.25mm;		//  ... just below surface

	ToolChange(CDData,"center drill");

	goto([0,0,-]);
	goto([-,-,TravelZ]);

	for (Tooth = 0 ; Tooth < GearTeeth ; Tooth++) {
		drill(CDPosition,TravelZ,2*TravelZ);		// large increment ensures one stroke
		CDPosition = rotate_xy(CDPosition,ToothAngle);
	}

//	now drill the holes

	ToolChange(DrillData,"drill");

	goto([0,0,-]);
	goto([-,-,TravelZ]);

	for (Tooth = 0 ; Tooth < GearTeeth ; Tooth++) {
		PeckDrill(HolePosition,TravelZ,DrillData[TOOL_DIA]);
		HolePosition = rotate_xy(HolePosition,ToothAngle);
	}

	GetAir();

}

//-- Mill center recess

if (DO_MILLCENTER) {

	MillData = [4.50mm,3000,250.0mm,0.0mm];			// spherical ball burr

	Delta = GearThick - GearCenterThick;							// depth to be milled away
	Inset = sqrt(2.0*Delta*(MillData[TOOL_DIA]/2) - pow(Delta,2));	// toll axis to milled edge

	ToolChange(MillData,"ball burr");

	goto([0,0,-]);							// above central hole
	goto([0,0,GearThick]);					// vertically down to flush with surface
	move([0,0,GearCenterThick]);			// into gear blank

	for (Angle = 0.0deg; Angle < 360.0deg; Angle+=360.0deg/16) {	// clear interior
		circle_cw((GearID/2 - Inset)/2,Angle);
	}

	move_r([(GearID/2 - Inset),0.0,0.0]);							// clean rim
	circle_ccw([0.0,0.0,GearCenterThick],2);

	GetAir();

}

//-- Drill outer ring

if (DO_DRILLOUTER) {

	RingRadius += DrillData[TOOL_DIA]/2;		// at OD of inner ring holes

	DrillData = DrillParam(3.18mm);
	RingRadius += DrillData[TOOL_DIA]/2.0;		// center of outer ring holes
	HolePosition = [RingRadius,0mm,-1.5*DrillData[TOOL_TIP]];

	ToolChange(DrillData,"drill");

	for (Tooth = 0 ; Tooth < GearTeeth ; Tooth++) {
		PeckDrill(HolePosition,TravelZ,DrillData[TOOL_DIA]);
		HolePosition = rotate_xy(HolePosition,ToothAngle);
	}

	GetAir();

}

//-- Drill to locate gear tooth tip end

if (DO_DRILLTIPS) {

	DrillData = DrillParam(4.22mm);

	RingRadius = GearOD/2.0 + DrillData[TOOL_DIA]/2.0;		// tangent to gear tooth tip
	HolePosition = [RingRadius,0mm,-1.5*DrillData[TOOL_TIP]];
	HolePosition = rotate_xy(HolePosition,ToothAngle/2);	// align to tooth

	ToolChange(DrillData,"drill");

	for (Tooth = 0 ; Tooth < GearTeeth ; Tooth++) {
		PeckDrill(HolePosition,TravelZ,DrillData[TOOL_DIA]);
		HolePosition = rotate_xy(HolePosition,ToothAngle);
	}

	GetAir();

}

literal("G30\n");
comment("msg,Done!");

The original doodle that suggested the possibility:

Can Opener Gears - Doodle 1

Can Opener Gears – Doodle 1

The chord equation at the bottom shows how to calculate the offset for the ball burr, although it turns out there’s no good way to measure the cutting diameter of the burr and it’s not really spherical anyway.

A more detailed doodle with the key line at a totally bogus angle:

Can Opener Gears - Doodle 2

Can Opener Gears – Doodle 2

The diagram in the lower right corner shows how you figure the length of the tip on a 118° drill point, which you add to the thickness of the plate in order to get a clean hole.

, , ,

4 Comments

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!

,

3 Comments

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.

,

Leave a comment

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

, ,

Leave a comment

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.

,

Leave a comment

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

}

,

1 Comment