Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.
As you might expect, changing the layer thickness to 0.1 mm = 100 μm dramatically improves the appearance of the dummy 9 mm Luger bullet on the left, compared to the 0.25 mm = 250 μm layers on the right:
Dummy 9 mm Luger cartridges – 0.1 mm layer – overview
The inside edge of the translucent skirt around the quartet measured 90 to 110 μm, so the layer height is spot on:
Dummy 9 mm Luger bullets – 0.1 mm layer – overhead on platform
That required no adjustments to the M2 at all; It Just Works. Admittedly, that’s with a custom platform and firm supports replacing the springs, plus better Z-axis homing, but the overall structure was fine to start with.
I used the same Slic3r settings as before, with the only change being the layer thickness. Letting it pick the layer width might produce better results, but a 0.35 mm nozzle won’t go much narrower than 0.40 mm anyway.
A closer look at the bullet show the thinner layers provide a better rendition of the stretched sphere forming the nose; it’s less pointy than the one assembled from thicker layers:
Dummy 9 mm Luger bullets – 0.1 mm layer – side
The nose closes better with thinner layers:
Dummy 9 mm Luger bullets – 0.1 mm layer – nose
None of that really matters for this application, but it’s a useful data point.
The downside is that printing with thinner layers requires more time: a single bullet (of 16) requires 2.2 minutes at 250 μm and (of 4) 9 minutes at 100 μm. The simple ratio of layer thicknesses predicts a factor of 2.5, not 4, but the skirt requires a larger fraction of the total time. The estimated time for a 4×4 array at 100 μm comes out at 5.2 minutes each, a factor of 2.4, which is close enough.
Although 100 μm certainly looks better, it doesn’t really improve anything for most of the blocky stuff I make…
That’s a snap cap on the left and a real 9 mm Luger cartridge on the right. The holes in the dummy brass indicate that they are absolutely, positively, unquestionably not loaded cartridges.
Start by drilling a 1/8 inch hole in the side of each unfired, primerless case:
Dummy 9 mm Luger – drilling case
I set up the chuck on the rotary table, thinking I might drill three holes in each cartridge, but came to my senses. It’s lined up by eye, flush with the end of the jaws, and the hole is just above the inside of the base.
The solid model has the same overall length and proportion as a 115 grain FMJ bullet, but doesn’t match the proper ogive or base diameter. Basically, I stretched a 9 mm sphere and stuck it atop a slightly tapered base cylinder:
Dummy 9 mm Luger bullet – solid model
For reasons I don’t profess to understand, the sphere has a slightly different diameter at its equator than the top of the cylinder, even though they’re both the same BulletOD diameter with the same number of faces. Fortunately, that didn’t affect the final results.
Print up a handful of the things:
Dummy 9 mm Luger bullets – on platform
The shadow from the flash makes the bases look slightly fatter than they really are.
Using a thinner layer would look better in this orientation. They’d definitely look better if they were split, printed with the long axis parallel to the plate, and glued together, as the grain would run lengthwise; I’m not sure there’s enough room for alignment pins, though.
At this diameter and number of faces, the M2 produces almost perfectly accurate dimensions, so the bullets press-fit just like you’d expect. They’re twisted into a dab of urethane glue inside the brass that foams just enough to hold them place.
Rather than use a real seating die, I deployed a closed chuck on the drill press. The trick is to set the depth stop to produce slightly too-long cartridges, then shim the platform without changing the stop and seat the bullet to the proper depth:
Dummy 9 mm Luger – seating bullet
The OAL tolerance for various 9 mm Luger cartridges seems to range from 1.08 inch to 1.17 inch, so anything in that range should be fine. I used 1.10 inch.
These are not intended for firing. You could fire them with just a primer (in a non-drilled case) and (maybe) not melt or shatter the plastic, but they’re slightly larger than the nominal 8.82 mm land diameter and won’t obturate or spin-stabilize worth diddly: expect short range and keyholing.
The sectional density is a whopping 0.008, should you keep track of such things: 0.47 gram = 7.2 grain. Note that the US small arms definition of sectional density has units of pound/inch2, not the pound/foot2 you’ll find right next to values computed using inches; the magic number 1/7000 just converts from grains to pounds. In the rest of the (metric) world, it’s entirely different.
Quite some time ago I manage to break the finger flange on one of my scope probes and, what with it being made of an un-glueable engineering plastic, a simple repair job failed quickly. It’s entirely round and a perfect lathe project, but … this is easier:
HP Scope Probe Flange Repair
You can see remnants of that failed repair just below the fracture:
HP scope probe flanges – repair disk
Some epoxy around the rim of the flange, plus filling the missing sector, looks about as grubby as you’d expect:
HP Scope Probes – rear
That’s a tiny zit at about 1 o’clock which came off with fingernail pressure.
From the business end, it actually looks pretty snappy:
HP Scope Probes – front
I’m mildly tempted to preemptively reinforce the other probes…
The OpenSCAD source code joins two parts with coincident faces, but it worked out OK for once:
// Tek Scope Probe Flange
// Ed Nisley KE4ZNU November 2013
//- Extrusion parameters must match reality!
// Print with 2 shells and 3 solid layers
ThreadThick = 0.25;
ThreadWidth = 0.40;
HoleWindage = 0.2;
Protrusion = 0.1; // make holes end cleanly
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
//----------------------
// Dimensions
FlangeOD = 16.0;
FlangeID = 8.75;
FlangeThick = IntegerMultiple(1.25,ThreadThick);
DiskOD = FlangeOD + 4*ThreadWidth;
DiskThick = FlangeThick + 4*ThreadThick;
NumSides = 8*4;
//----------------------
// 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);
}
//----------------------
// Build it
ShowPegGrid();
difference() {
union() {
translate([0,0,2*ThreadThick])
cylinder(r=DiskOD/2,h=DiskThick,$fn=NumSides); // main repair part
cylinder(r1=(DiskOD - 2*ThreadWidth)/2,r2=DiskOD/2,h=2*ThreadThick,$fn=NumSides);
}
translate([0,0,(DiskThick - FlangeThick)]) // flange clearance
PolyCyl(FlangeOD,2*FlangeThick,NumSides);
translate([0,0,-DiskThick/2])
PolyCyl(FlangeID,2*DiskThick,NumSides);
}
I’ve been working on an object (more on this later) that requires precise alignment of two parts that capture a nut deep inside. This calls for alignment pins, similar to the ones I used for, say, the Triple-Cylinder Thing:
Cylinder Thing – rotated
The general idea is to design holes that fit the pins, then locate them at the parting line of the model, where they’re subtracted from the solid and appear in exactly the proper places when the model splits for printing:
Cylinder Thing – alignment pegs
You slather solvent glue on both halves, jam pins into the holes, slap the parts together, and clamp until cured. Works fine, I use pins all over the place.
The gotcha of using just a (polygonal) cylinder as the hole: if you glue one end of the pin at a time, a small rim of dissolved plastic may form around the pin at the surface. That can bond the two halves together or prevent them from joining properly after being disassembled.
Sooo, here’s a new alignment pin hole with a gutter around the pin on both surfaces to capture the glop:
Alignment pin hole – overview
Remember, that’s the negative volume that will hold the pin, not the pin itself!
Here’s how it works in real plastic, with a 1.75 mm peg glued into one hole with a bit of crud in the gutter:
Alignment Hole and Pin
The secret to making the gutter work: offset the second layer by half the thread width, so that it’s reasonably well supported on the first layer. If you don’t do that, the inner layers simply drop down through the hole and fill the gutter. Even doing that, notice the distortion of the first few layers inside the hole.
The OpenSCAD source code looks about like you’d expect:
Ideally, the pin length should extend at least two diameters into each side of the object, but you can feed in whatever you need to make it come out right.
The PolyCyl() routine produces a low-vertex-count polygon that circumscribes the nominal diameter, which is what you need for vertical holes in 3D printed objects:
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);
}
Although the current OpenSCAD could produce a solid model with the screw thread’s dedendum, I’d never actually printed one of them:
Broom Handle Screw – full thread – solid model
I need some fondlestuff illustrating how to handle overhangs, so I ran one standing vertically, which (pretty much as I expected) didn’t work well at all:
Broom Handle Screw – dedendum – vertical
The trick is to split the model down the middle:
Broom Handle Screw – horizontal top
And put holes in each half for alignment pins:
Broom Handle Screw – horizontal bottom
Then you can print it lying down:
Broom Handle Screw – horizontal – as-printed top
The internal overhang would probably call for some support material, particularly in the square recess at the end, but in this case it’s a lesson:
Glue some filament snippets into the holes, snap it together, and it looks just fine over there on the right:
Broom Handle Screw – orientation comparison
Doesn’t matter how many I print, it still doesn’t make any economic sense as a broom repair…
The OpenSCAD source code now has a Layout variable to control the orientation and, not as shown in the model, the alignment pins have glue gutters in the first layer:
// Broom Handle Screw End Plug
// Ed Nisley KE4ZNU October 2013
Layout = "Horizontal"; // Vertical Horizontal Pin
UseDedendum = true; // true to create full thread form
//- Extrusion parameters must match reality!
ThreadThick = 0.25;
ThreadWidth = 0.40;
HoleWindage = 0.2;
Protrusion = 0.1; // make holes end cleanly
//----------------------
// Dimensions
PostOD = 22.3; // post inside metal handle
PostLength = 25.0;
FlangeOD = 24.0; // stop flange
FlangeLength = 3.0;
PitchDia = 15.5; // thread center diameter
ScrewLength = 20.0;
ThreadFormOD = 2.5; // diameter of thread form
ThreadPitch = 5.0;
NumSegments = 32; // .. number of cylinder approximations per turn
BoltOD = 7.0; // clears 1/4-20 bolt
BoltSquare = 6.5; // across flats
BoltHeadThick = 3.0;
RecessDia = 6.0; // recesss to secure post in handle
OALength = PostLength + FlangeLength + ScrewLength;
SplitOC = 1.25*FlangeOD; // separation in Horizontal layout
PinOD = 1.75; // alignment pin diameter = filament stub
PinLength = 7.0; // ... length
$fn=8*4; // default cylinder sides
echo("Pitch dia: ",PitchDia);
echo("Root dia: ",PitchDia - ThreadFormOD);
echo("Crest dia: ",PitchDia + ThreadFormOD);
Pi = 3.14159265358979;
//----------------------
// Useful routines
// Wrap cylindrical thread segments around larger plug cylinder
module CylinderThread(Pitch,Length,PitchDia,ThreadOD,PerTurn) {
CylFudge = 1.02; // force overlap
RotIncr = 1/PerTurn;
PitchRad = PitchDia/2;
Turns = Length/Pitch;
NumCyls = Turns*PerTurn;
ZStep = Pitch / PerTurn;
HelixAngle = atan(Pitch/(Pi*PitchDia));
CylLength = CylFudge * (Pi*(PitchDia + ThreadOD) / PerTurn) / cos(HelixAngle);
for (i = [0:NumCyls-1]) {
assign(Angle = 360*i/PerTurn)
translate([PitchRad*cos(Angle),PitchRad*sin(Angle),i*ZStep])
rotate([90+HelixAngle,0,Angle])
cylinder(r1=ThreadOD/2,
r2=ThreadOD/(2*CylFudge),
h=CylLength,
center=true,$fn=12);
}
}
// Build complete plug
module ScrewPlug() {
difference() {
union() {
cylinder(r=PostOD/2,h=PostLength);
cylinder(r=PitchDia/2,h=OALength);
translate([0,0,PostLength])
cylinder(r=FlangeOD/2,h=FlangeLength);
color("Orange")
translate([0,0,(PostLength + FlangeLength)])
CylinderThread(ThreadPitch,(ScrewLength - ThreadFormOD/2),PitchDia,ThreadFormOD,NumSegments);
}
translate([0,0,-Protrusion])
PolyCyl(BoltOD,(OALength + 2*Protrusion),6);
translate([0,0,(OALength - BoltHeadThick)])
PolyCyl(BoltSquare,(BoltHeadThick + Protrusion),4);
if (UseDedendum)
translate([0,0,(PostLength + FlangeLength + ThreadFormOD/2 - ThreadPitch/(2*NumSegments))])
rotate(-90 - 360/(2*NumSegments))
CylinderThread(ThreadPitch,ScrewLength,PitchDia,ThreadFormOD,NumSegments);
for (i = [0:90:270]) {
rotate(45 + i) // 45 works better with Horizontal layout
translate([PostOD/2,0,PostLength/2])
sphere(r=RecessDia/2,$fn=8);
}
}
}
// Locating pin hole with glue recess
module LocatingPin() {
translate([0,0,-ThreadThick])
PolyCyl((PinOD + 2*ThreadWidth),2*ThreadThick,4);
translate([0,0,-(PinLength/2 + ThreadThick)])
PolyCyl(PinOD,(PinLength + 2*ThreadThick),4);
}
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);
}
//-------------------
// Build it...
ShowPegGrid();
if (Layout == "Vertical")
ScrewPlug();
if (Layout == "Pin")
LocatingPin();
if (Layout == "Horizontal")
for (i=[-1,1])
difference() {
translate([i*SplitOC/2,PostLength/2,0])
rotate([90,180*(i + 1)/2,0])
ScrewPlug();
translate([0,0,-FlangeOD/2])
cube([2*OALength,2*OALength,FlangeOD],center=true);
for (j=[-1,1], pin=[-1,1])
assign(PinX = i*SplitOC/2 + pin*(PostOD + BoltOD)/4,
PinY = j*PostLength/4) {
translate([PinX,PinY,0])
rotate(45)
LocatingPin();
echo("i j pin: ",i,j,pin);
echo("X Y: ",PinX,PinY);
}
}
The houseplants have migrated indoors after spending a summer charging up in the sun on the patio, which means it’s time to replace the silicone rubber feet on the bottom of the plant shelves. This year, I printed a set of feet to fit the hex-head adjustable feet:
Plant Stand Foot – installed
The pencil-stem plant on the left, for whatever it’s worth, is a perfectly healthy Rhipsalis that greatly enjoyed the summer sun.
The feet print upside-down to give the surface around the hex a smooth finish. I used Slic3r’s Hilbert Curve for pattern a bit more interesting than the usual parallel lines:
Plant Shelf Foot – as built
The Hilbert curve doesn’t fit neatly into a non-rectangular shape, but it’s close enough.
The solid model includes the support structure:
Plant Shelf Foot – solid model – bottom
Which pops out cleanly:
Plant Shelf Foot – support material detail
Yes, that’s a shred of red filament embedded on the left side. Cleanliness is next to impossible…
The fuzzy felt feet come from a 6 mm thick slab of the stuff:
Plant Shelf Foot – cutting felt plugs
The round socket wall leaves about 2 mm of felt showing at the bottom; it’s not very compressible and that should suffice to keep the plastic off the table.
The OpenSCAD source code:
// Feet for a wire-shelf plant stand
// Ed Nisley KE4ZNU October 2013
Layout = "Build"; // Show Build
Support = true;
//- Extrusion parameters must match reality!
// Print with 2 shells and 3 solid layers
ThreadThick = 0.25;
ThreadWidth = 0.40;
HoleWindage = 0.2;
Protrusion = 0.1; // make holes end cleanly
//----------------------
// Dimensions
StandFootOD = 18.0; // hex across flats
StandFootDepth = 5.0; // ... socket depth
FeltPadOD = 25.0; // felt foot diameter
FeltPadDepth = 4.0; // ... depth
FootBaseThick = 6*ThreadThick; // between foot and pad
FootWall = 4*ThreadWidth; // around exterior
FootOD = 2*FootWall + max(StandFootOD,FeltPadOD);
echo(str("Foot OD: ",FootOD));
FootTall = StandFootDepth + FootBaseThick + FeltPadDepth;
echo(str(" ... height: "),FootTall);
NumSides = 8*4;
//----------------------
// Useful routines
module FootPad() {
difference() {
cylinder(r=FootOD/2,h=FootTall,$fn=NumSides);
translate([0,0,FeltPadDepth + FootBaseThick])
PolyCyl(StandFootOD,2*StandFootDepth,6);
translate([0,0,-Protrusion])
PolyCyl(FeltPadOD,(FeltPadDepth + Protrusion),NumSides);
}
}
// Locating pin hole with glue recess
module LocatingPin() {
translate([0,0,-ThreadThick])
PolyCyl((PinOD + 2*ThreadWidth),2*ThreadThick,4);
translate([0,0,-(PinLength/2 + ThreadThick)])
PolyCyl(PinOD,(PinLength + 2*ThreadThick),4);
}
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);
}
//-------------------
// Build it...
ShowPegGrid();
if (Layout == "Show")
FootPad();
if (Layout == "Build") {
translate([0,0,FootTall])
rotate([180,0,0])
FootPad();
if (Support)
color("Yellow")
for (Seg=[0:5]) {
rotate(30 + 360*Seg/6)
translate([0,0,(StandFootDepth - ThreadThick)/2])
cube([(StandFootOD - 3*ThreadWidth),
2*ThreadWidth,
(StandFootDepth - ThreadThick)],
center=true);
}
}
A few trips with the M2 convinced me that the cable to the relocated Z-min switch along the front of the X gantry needed a clip on each end and should not run under the gantry. This time I used the full width of the steel strap and bashed a neater curve around a length of drill rod:
M2 Z-min Cable Clip – forming
The new clips look a bit better with straight edges:
M2 Z-min Cable Clips – old vs new
The top view shows the new clips and cable location:
M2 Z-min Switch – top view
While I was at it, I trimmed the edges off the switch mounting block. Rather than figure out the trig required to hack off the corners, I applied linear_extrude() to a polygon() defined by some obvious points, then poked the same holes in the block:
Z-min Front Mount Switch Block – chamfer – solid model
It pretty much vanishes in the top view, but here’s a view from the +Y end of the platform:
M2 Z-min Switch – bottom view
Despite all that maneuvering, the G92 Z-4.55 touchoff value remained the same!
If you’ve forgotten why all this makes sense, it’s a first pass at detecting the actual build platform position. The stock M2 uses that switch to detect the top of a screw attached to the Z-axis stage, which means it can’t sense the actual platform. The Z-min switch I added to the Thing-O-Matic convinced me that was the only way to fly; given the TOM’s plywood-and-acrylic frame, it was essentially mandatory.
Mounting the switch on the extruder would allow probing the entire platform, which would allow on-the-fly correction for both average height and (non-)flatness, but that’s a whole ‘nother project.
The OpenSCAD source code:
// Block to mount M2 Z-min switch on X gantry
// Ed Nisley KE4ZNU - Oct 2013
//- Extrusion parameters - must match reality!
ThreadThick = 0.25;
ThreadWidth = 0.40;
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
Protrusion = 0.1;
HoleWindage = 0.2;
//- Sizes
SwitchLength = 20.0; // switch size across front of block
SwitchScrewOD = 2.05; // microswitch screw tapping
SwitchScrewOC = 9.5; // ... on-center spacing
GantryScrewOD = 3.0; // X rail screw clearance
GantryScrewOC = 25.0; // ... on-center spacing along X
GantryScrewOffset = 12.0; // ... Y offset from gantry front
BlockSize = [1.5*GantryScrewOC,17.0,5.0]; // XYZ dimensions as mounted
HalfBlock = BlockSize/2;
SwitchScrewLength = BlockSize[1] - 5*ThreadWidth; // net length of switch screws
echo("Max switch screw length: ",SwitchScrewLength + 5.0); // ... allow switch thickness
ChamferAngle = atan((BlockSize[0] - SwitchLength)/(BlockSize[1]/2));
echo("Chamfer Angle: ",ChamferAngle);
//- Adjust hole diameter to make the size come out right
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);
}
//- 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);
}
//- Define basic block shape
module BaseBlock() {
translate([0,-GantryScrewOffset,0])
linear_extrude(height=BlockSize[2])
polygon(points=[[-HalfBlock[0],BlockSize[1]],
[HalfBlock[0],BlockSize[1]],
[HalfBlock[0],HalfBlock[1]],
[SwitchLength/2,0],
[-SwitchLength/2,0],
[-HalfBlock[0],HalfBlock[1]]
]);
}
//- Build it
ShowPegGrid();
difference() {
BaseBlock();
for (i=[-1,1]) {
translate([i*GantryScrewOC/2,0,-Protrusion])
rotate(-90)
PolyCyl(GantryScrewOD,(BlockSize[2] + 2*Protrusion));
translate([i*SwitchScrewOC/2,-(GantryScrewOffset + Protrusion),BlockSize[2]/2])
rotate([-90,0,0])
rotate(90)
PolyCyl(SwitchScrewOD,(SwitchScrewLength + Protrusion));
}
}