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.

Category: Software

General-purpose computers doing something specific

  • Mini-lathe Change Gear Generator: Redux

    Mini-lathe Change Gear Generator: Redux

    Because the BOSL2 library includes a gear generator, I can now avoid creating a gear outline in Inkscape and importing it into my stacked change gear generator.

    The labels now snuggle closer to the shaft and (barely) fit on smaller gears:

    Mini-lathe stacked change gears - 28T - solid model
    Mini-lathe stacked change gears – 28T – solid model

    The stacked B-C gears for the jack shaft work as before, with both labels on the top gear:

    Mini-lathe stacked change gears - 28-50T - solid model
    Mini-lathe stacked change gears – 28-50T – solid model

    The admittedly flimsy motivation for all this was to make a 28 tooth gear to cut a 0.9 mm pitch, thus filling an obvious hole in the gear table.

    My collection of gears could do 21-60-81-50, but the 81 T gear collides with the screw holding the 21 T gear. Rearranging it to 21-50-81-60 showed the B-C gears exceeded the space available.

    Because it’s all ratios and a 28 T gear is 4/3 bigger than 21 T, reducing the rest of the train by 3/4 should work. In fact, it produced a reasonable 28-80-81-50 chain:

    Mini-lathe change gears - 28T installed
    Mini-lathe change gears – 28T installed

    The fact that I do not anticipate ever needing to cut a 0.9 mm pitch has nothing whatsoever to do with it; that gear will surely come in handy for something.

    While I was at it, I made a 27 T gear, because 27 = 21 × 9/7:

    Mini-lathe stacked change gears - 27T - PrusaSlicer preview
    Mini-lathe stacked change gears – 27T – PrusaSlicer preview

    You can never have enough change gears. Right?

    The OpenSCAD source code as a GitHub Gist:

    // LMS Mini-Lathe
    // Change gears with stacking
    // Ed Nisley – KE4ZNU
    // 2020-05 use Inkscape SVG gears
    // 2025-12 use BOSL2 gear generator
    include <BOSL2/std.scad>
    include <BOSL2/gears.scad>
    /* [Gears] */
    TopGear = 0; // zero for single gear
    BottomGear = 28;
    /* [Hidden] */
    ThreadThick = 0.20;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    /* [Dimensions] */
    ShaftOD = 12.0;
    GearThick = 7.75;
    Keyway = [3.5,3.0,3*GearThick]; // x on radius, y on perim
    LegendEnable = (TopGear == 0 && BottomGear > 27) || (TopGear > 27);
    LegendThick = 2*ThreadThick;
    LegendZ = (TopGear ? 2*GearThick : GearThick) – LegendThick;
    LegendSize = 5;
    LegendRecess = [8,6,LegendThick];
    LegendOffset = [0,LegendRecess.y/2 + ShaftOD/2 + HoleWindage,LegendZ + LegendRecess.z/2];
    //———————–
    // Build it!
    union() {
    difference() {
    union() {
    spur_gear(mod=1,teeth=BottomGear,thickness=GearThick,shaft_diam=ShaftOD + HoleWindage,anchor=BOTTOM);
    if (TopGear)
    spur_gear(mod=1,teeth=TopGear,thickness=2*GearThick,shaft_diam=ShaftOD + HoleWindage,anchor=BOTTOM);
    }
    right(ShaftOD/2)
    down(Protrusion)
    cube(Keyway,anchor=CENTER+BOTTOM);
    if (LegendEnable) {
    translate(LegendOffset)
    cube(LegendRecess + [0,0,Protrusion],anchor=CENTER);
    if (TopGear)
    zrot(180)
    translate(LegendOffset)
    cube(LegendRecess + [0,0,Protrusion],anchor=CENTER);
    }
    }
    if (LegendEnable)
    translate([0,0,LegendZ – Protrusion])
    linear_extrude(height=LegendThick + Protrusion,convexity=10) {
    translate([LegendOffset.x,LegendOffset.y])
    text(text=str(BottomGear),size=LegendSize,font="Arial:style:Bold",halign="center",valign="center");
    if (TopGear)
    zrot(180)
    translate([LegendOffset.x,LegendOffset.y])
    text(text=str(TopGear),size=LegendSize,font="Arial:style:Bold",halign="center",valign="center");
    }
    }
  • Sears Humidifier Bottle Cap Reinforcement

    Sears Humidifier Bottle Cap Reinforcement

    In the midst of the humidification season, I spotted this while refilling one of the ancient Sears Humidifier bottles:

    Humidifier bottle cap reinforcement - crack
    Humidifier bottle cap reinforcement – crack

    While it’s possible to buy replacement caps, this seemed more appropriate:

    Humidifier bottle cap reinforcement - installed
    Humidifier bottle cap reinforcement – installed

    It’s PETG-CF, of course:

    Bottle cap reinforcement - solid model
    Bottle cap reinforcement – solid model

    The shape is a ring with a simplified model of the cap removed from the middle:

    Bottle cap reinforcement - lid solid model
    Bottle cap reinforcement – lid solid model

    It fits snugly over the cap atop a thin layer of JB PlasticBonder that should hold it in place forevermore:

    Humidifier bottle cap reinforcement - bottom view
    Humidifier bottle cap reinforcement – bottom view

    The other side shows the crack over on the right:

    Humidifier bottle cap reinforcement - top view
    Humidifier bottle cap reinforcement – top view

    Close inspection showed a few smaller cracks, so that cap was likely an original.

    I made another ring for the other cap, only to find it was slightly larger with a black washer inside: apparently a previous owner had replaced one of the caps. The OpenSCAD program has measurements for both, not that you have either.

    The OpenSCAD source code as a GitHub Gist:

    // Humidifier bottle cap reinforcement
    // Ed Nisley – KE4ZNU
    // 2025-11-29
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build,Cap]
    /* [Hidden] */
    Protrusion = 0.1;
    //—–
    // Bottle cap/valve
    // Collects all the magic numbers in one place
    Left = false; // the caps are different, of course
    CapODs = Left ? [43.0,42.1] : [43.1,42.9]; // [0] = base of cap
    CapHeight = 10.0;
    Notch = [0.6,2.0,8.5 + Protrusion]; // Z + hack for slight angle
    NumRibs = 24;
    RibAngle = 90 – atan(CapHeight/((CapODs[0]-CapODs[1])/2));
    echo(RibAngle=RibAngle);
    $fn=2*NumRibs;
    module Cap() {
    difference() {
    cyl(CapHeight,d1=CapODs[1],d2=CapODs[0],anchor=BOTTOM);
    for (a=[0:NumRibs-1])
    zrot(a*360/NumRibs)
    right(CapODs[1]/2) down(Protrusion)
    yrot(RibAngle)
    cuboid(Notch,anchor=RIGHT+BOTTOM);
    }
    }
    //—–
    // Reinforcing ring
    RingThick = 3.0;
    module Ring() {
    render()
    difference() {
    tube(CapHeight,od=CapODs[0] + 2*RingThick,id=CapODs[1] – 2*Notch.x,anchor=BOTTOM);
    Cap();
    }
    }
    // Build things
    if (Layout == "Cap")
    Cap();
    if (Layout == "Build" || Layout == "Show")
    Ring();
  • Bird Feeder Tray Mount

    Bird Feeder Tray Mount

    The mixed flock attending the bird feeder in the back yard scatters enough seeds to attract the deer, so I added a tray underneath to catch the overspray:

    Bird Feeder Tray Mount - installed
    Bird Feeder Tray Mount – installed

    Well, two trays, because it took a couple of iterations to make the solid model match reality:

    Bird Feeder Tray Mount - show layout
    Bird Feeder Tray Mount – show layout

    The n-1 iteration was Close Enough™ and two trays are obviously better than one.

    The “trays” are stray lids from the six gallon buckets we use for many purposes, including root-cellaring the vegetable garden harvest. The lid’s solid model was straightforward:

    Bird Feeder Tray Mount - lid model
    Bird Feeder Tray Mount – lid model

    Removing the lid from a solid block produces the most complex part of the mount:

    Bird Feeder Tray Mount - mount layout
    Bird Feeder Tray Mount – mount layout

    An aluminum plate on the outside (the gray slab in the overall view above) distributes the strain from the two M6 screws across the block.

    A smaller block on the inside of the lid has a pair of square nuts:

    Bird Feeder Tray Mount - segment layout
    Bird Feeder Tray Mount – segment layout

    All three parts build from their flattest side:

    Bird Feeder Tray Mount - build layout
    Bird Feeder Tray Mount – build layout

    The downward facing clamp arch in the main block didn’t need support, but the square nut sockets in the segment definitely came out better with little support blocks inside; PrusaSlicer does a good job with most support structures.

    The n-1 iteration used M6 rivnuts that were slightly too long after making the lid model match reality, so I switched to square nuts. The OpenSCAD code calculates the segment block length to match the actual screws, but 75 mm M6 screws and square nuts are barely long enough.

    I clamped the outer block to the lid as a drill guide for the first hole, then pinned the block with a screw to ensure it didn’t slip while drilling the second hole:

    Bird Feeder Tray Mount - drilling setup
    Bird Feeder Tray Mount – drilling setup

    Those were freehanded in the drill press at low speed with serious concentration; some things you just gotta do that way.

    The mixed flock overwhelmingly approves the trays, to the extent a dozen birds clamor to use them: definitely a crowd-pleaser!

    I’m certain you can buy pole-mounted trays, but what’s the fun in that?

    The OpenSCAD source code as a GitHub Gist:

    // Bird feeder tray mount
    // Ed Nisley – KE4ZNU
    // 2025-11-06
    include <BOSL2/std.scad>
    Layout = "Show"; // [Build,Show,Lid,Mount,Segment,Nut]
    /* [Hidden] */
    ID = 0;
    OD = 1;
    LENGTH = 2;
    HoleWindage = [0.2,0.2,0.2];
    Protrusion = 0.1;
    NumRibs = 12; // stiffening ribs
    NumSides = 8*NumRibs;
    $fn=NumSides;
    Gap = 5.0;
    WallThick = 9.0; // robust walls
    Kerf = 1.0; // clamp cut
    TapeThick = 0.5; // wrap around pole
    LidOD = 12; // main diameter in inches
    PoleOD = 1*INCH;
    PlateThick = 2.0; // backing plate for clamp
    Screw = [6.0,12.0,6.0]; // thread OD, washerOD, head
    ScrewLength = 75.0;
    ScrewOC = 60.0; // chosen to clear stiffening ribs in lid
    LidLayers = [ // bottom to top, ID = 0 means solid disk, LENGTH = exterior measurement
    [0,(LidOD-2*(3/8))*INCH,Protrusion], // 0 – below zero to prevent Z fighting
    [0,(LidOD-2*(3/8))*INCH,(3/8)*INCH], // 1 – base inside bucket
    [0,(LidOD+2*(1/8))*INCH,(1/8)*INCH], // 2 – flange
    [(LidOD-2*(1/2))*INCH,LidOD*INCH,(7/8)*INCH], // 3 – sealing ring
    ];
    LidOAH = LidLayers[1][LENGTH] + LidLayers[2][LENGTH] + LidLayers[3][LENGTH];
    LidTopDepth = (3/4)*INCH; // from highest part of interior
    MountBlockWidth = ScrewOC + 2*WallThick;
    BaseSagitta = LidLayers[1][OD]/2 – sqrt((LidLayers[1][OD]/2)^2 – (MountBlockWidth^2)/4);
    echo(BaseSagitta=BaseSagitta);
    PoleOffset = BaseSagitta + ((LidLayers[2][OD] – LidLayers[1][OD])/2) + WallThick + PoleOD/2;
    MountBlock = [PoleOffset + PoleOD/2 + WallThick – PlateThick,MountBlockWidth,LidOAH];
    echo(MountBlock=MountBlock);
    SegBlockOffset = ScrewLength – MountBlock.x – PlateThick; // assumes recessed
    SegmentBlock = [2*SegBlockOffset,MountBlock.y,LidTopDepth];
    Rib = [2*6.0,5.0,LidTopDepth]; // lid stiffening ribs
    RibAlign = 0 * 180/NumRibs; // position ribs wrt mount
    EdgeRadius = 3.0;
    //—–
    // Rivnut
    // The model collects all the magic numbers right here
    /*
    RivnutOAL = 15.0;
    module Rivnut() {
    union() {
    cyl(1.6,d=13.0,circum=true,anchor=BOTTOM);
    cyl(RivnutOAL,d=9.0,circum=true,anchor=BOTTOM);
    }
    }
    */
    //—–
    // Square nut
    // The model collects all the magic numbers right here
    NutOAL = 5.0;
    module SquareNut() {
    cuboid([10.0,10.0,5.0],anchor=BOTTOM);
    }
    //—–
    // Bucket lid
    // Centered at XY=0, Z=0 at top of exterior flange
    module BucketLid(Interior=true,Expand=false) {
    render()
    union() {
    down(LidLayers[2][LENGTH])
    cyl(LidLayers[1][LENGTH],d=LidLayers[1][OD],anchor=TOP);
    cyl(LidLayers[2][LENGTH],d=LidLayers[2][OD],anchor=TOP);
    if (Interior) {
    if (false)
    down(Expand ? Protrusion : 0)
    tube(LidLayers[3][LENGTH] + (Expand ? 2*Protrusion : 0),
    id=LidLayers[3][ID],od=(Expand ? 2 : 1)*LidLayers[3][OD],anchor=BOTTOM);
    else
    difference() {
    cyl(LidLayers[3][LENGTH] + (Expand ? 2*Protrusion : 0),
    d=(Expand ? 2 : 1)*LidLayers[3][OD],anchor=BOTTOM);
    up(LidLayers[3][LENGTH] – LidTopDepth)
    cyl(LidTopDepth + (Expand ? 2*Protrusion : 0),
    d=LidLayers[3][ID],anchor=BOTTOM);
    }
    up(LidLayers[3][LENGTH] – LidTopDepth)
    for (i=[0:(NumRibs – 1)])
    zrot(i*360/NumRibs + RibAlign)
    right(LidLayers[3][ID]/2)
    cuboid(Rib,anchor=BOTTOM,rounding=1,edges="Z");
    }
    else
    down(Expand ? Protrusion : 0)
    cyl(LidLayers[3][LENGTH] + (Expand ? 2*Protrusion : 0),
    d=(Expand ? 2 : 1)*LidLayers[3][OD],anchor=BOTTOM);
    }
    }
    // Mount clamp
    module Mount() {
    render()
    difference() {
    cuboid(MountBlock,anchor=BOTTOM+LEFT,rounding=EdgeRadius,edges="X");
    left(LidLayers[1][OD]/2 – BaseSagitta)
    up(LidLayers[1][LENGTH] + LidLayers[2][LENGTH])
    BucketLid(Interior=false);
    right(PoleOffset) {
    cyl(3*MountBlock.z,d=(PoleOD + HoleWindage.x + 2*TapeThick),circum=true,anchor=CENTER);
    cuboid([Kerf,2*MountBlock.y,3*MountBlock.z]);
    }
    if (false)
    right(MountBlock.x – PlateThick)
    cuboid(3*[PlateThick,MountBlock.y,MountBlock.z],anchor=LEFT);
    up(LidOAH – LidLayers[3][LENGTH]/2)
    for (j=[-1,1])
    fwd(j*ScrewOC/2) {
    cyl(ScrewLength,d=Screw[ID] + HoleWindage.x,circum=true,orient=RIGHT,anchor=BOTTOM,$fn=6,spin=180/6);
    if (false)
    right(MountBlock.x + Protrusion)
    cyl(Screw[LENGTH] + Protrusion,d=Screw[OD] + HoleWindage.x,circum=true,
    orient=LEFT,anchor=BOTTOM,$fn=12,spin=180/12);
    }
    }
    }
    // Nut block segment inside lid
    module NutSegment() {
    render()
    difference() {
    cuboid(SegmentBlock,anchor=BOTTOM,rounding=EdgeRadius,edges="X");
    down(LidLayers[3][LENGTH] – LidTopDepth)
    left(LidLayers[1][OD]/2 – BaseSagitta)
    BucketLid(Interior=true,Expand=true);
    up(LidTopDepth – LidLayers[3][LENGTH]/2)
    for (j=[-1,1])
    fwd(j*ScrewOC/2) {
    left(SegmentBlock.x/2)
    cyl(ScrewLength,d=Screw[ID],circum=true,anchor=BOTTOM,$fn=6,spin=180/6,orient=RIGHT);
    left(SegmentBlock.x/2)
    yrot(90)
    SquareNut();
    }
    }
    }
    //—–
    // Build things
    if (Layout == "Lid")
    BucketLid();
    if (Layout == "Mount")
    Mount();
    if (Layout == "Segment")
    NutSegment();
    if (Layout == "Nut")
    Rivnut();
    if (Layout == "Show") {
    down(LidLayers[1][LENGTH] + LidLayers[2][LENGTH]) {
    Mount();
    color("Orange",0.5)
    up(LidOAH – LidLayers[3][LENGTH]/2)
    right(MountBlock.x + PlateThick)
    for (j=[-1,1])
    fwd(j*ScrewOC/2)
    cyl(ScrewLength,d=Screw[ID],circum=true,orient=LEFT,anchor=BOTTOM);
    }
    up(LidLayers[3][LENGTH] – LidTopDepth)
    NutSegment();
    color("Gray",0.4)
    right(PoleOffset)
    cylinder(3*MountBlock.z,d=(PoleOD),anchor=CENTER);
    color("Gray",0.4)
    left(LidLayers[1][OD]/2 – BaseSagitta)
    BucketLid();
    color("White",0.7)
    down(LidLayers[1][LENGTH] + LidLayers[2][LENGTH])
    right(MountBlock.x + 2*PlateThick)
    difference() {
    cuboid([PlateThick,MountBlock.y,MountBlock.z],anchor=BOTTOM+LEFT,rounding=EdgeRadius,edges="X");
    up(LidOAH – LidLayers[3][LENGTH]/2)
    for (j=[-1,1])
    fwd(j*ScrewOC/2)
    cyl(ScrewLength,d=Screw[ID],circum=true,orient=RIGHT,anchor=CENTER);
    }
    }
    if (Layout == "Build") {
    render()
    union() {
    difference() {
    left(MountBlock.z + Gap/2)
    up(PoleOffset – Kerf/2)
    yrot(90)
    Mount();
    cuboid([3*MountBlock.z,2*MountBlock.y,3*MountBlock.x],anchor=TOP);
    }
    render()
    right(Gap/2)
    intersection() {
    up(MountBlock.x)
    yrot(90)
    Mount();
    up(MountBlock.x – PoleOffset)
    right(MountBlock.z/2)
    cuboid([2*MountBlock.z,2*MountBlock.y,MountBlock.x],anchor=TOP);
    }
    right(2*MountBlock.z – BaseSagitta)
    up(SegmentBlock.x/2)
    yrot(-90)
    NutSegment();
    }
    }

  • Generator Air Filter Screw Knob

    Generator Air Filter Screw Knob

    Part of the Autumn festivities around here involves blowing leaves into piles, then shredding them into garden mulch. Given that I have a plug-in electric leaf blower / wind stick, I use this as an excuse to exercise the emergency generator (similar to that one) with a (relatively) short extension cord.

    As with all small gasoline engines, I fire a shot of starting fluid into the air cleaner to reduce the number of engine-start yanks, which means I must remove the generator’s side panel and unscrew the filter cover. For years I have sworn mighty oaths on the bones of my ancestors to knobify that screw, thus eliminating fiddling with a screwdriver.

    Finally:

    Generator Air Filter Screw Knob - solid model
    Generator Air Filter Screw Knob – solid model

    A dozen minutes of printing and a snippet of good double-sided tape later:

    Generator air filter knob - installed
    Generator air filter knob – installed

    The knob sticks out far enough to push into the foam “sound deadening” liner on the cover, so it won’t vibrate loose.

    The OpenSCAD source code:

    include <BOSL2/std.scad>
    
    /* [Hidden] */
    
    HoleWindage = 0.2;
    Protrusion = 0.1;
    
    // Screw head dome
    
    HeadHeight = 2.0;
    HeadOD = 14.75;
    
    DomeRadius = (HeadHeight^2 + (HeadOD^2)/4) / (2*HeadHeight);
    echo(DomeRadius=DomeRadius);
    
    KnobOD = HeadOD;
    KnobLength = 15.0;
    
    RimFudge = 0.3;   // ensures a printable edge
    
    // Build it
    
    difference() {
      cyl(h=KnobLength, r=KnobOD/2,anchor=BOTTOM,texture="trunc_pyramids",tex_size=[2.0,KnobLength/4]);
    #  up(KnobLength - HeadHeight + RimFudge)
        spheroid(r=DomeRadius,circum=true,style="icosa",anchor=BOTTOM);
    }
    

    The cover has robust plastic latches, so I haven’t ever bothered to tighten those screws.

  • Handlebar Grip Sleeve

    Handlebar Grip Sleeve

    Mary’s zero-mph crash loosened the starboard handlebar plug enough to let it eventually decamp for parts unknown. Its replacement, a somewhat fancier aluminum plug with an expanding cone retainer using an actual M3 nut, worked fine for the last year, but Mary recently noticed the socket head screw had worked loose.

    In the interim, I’d moved the Bafang thumb control from its original position on the crossbar to just above the rear shifter:

    Tour Easy - right handlebar control stack
    Tour Easy – right handlebar control stack

    Which moved the clamp on the shortened grip off the end of the handlebar tube, so I flipped the grip around, tightened the clamp, and installed the plug.

    Unfortunately, the grip ID is 4 mm larger than the tube ID, which meant the plug’s cone retainer was struggling to hold on in there. Perhaps the plastic cone has relaxed bit, but I figured giving it more traction would be a Good Idea™ before I declared victory:

    Handlebar Grip Sleeve - PrusaSlicer
    Handlebar Grip Sleeve – PrusaSlicer

    It’s a little plastic sleeve with slots to let it expand against the inside of the grip:

    Handlebar grip sleeve - installed
    Handlebar grip sleeve – installed

    Yes, it’s sticking out slightly; you can see the corresponding gap up inside next to the tube.

    A wrap of double-sided sticky tape glues it in place as the retainer presses it against the grip ID and a dot of low-strength Loctite should keep the screw from loosening again.

    The OpenSCAD source code:

    // Handlebar grip sleeve
    // Ed Nisley - KE4ZNU
    // 2025-10-25
    
    include <BOSL2/std.scad>
    
    /* [Hidden] */
    
    ID = 0;
    OD = 1;
    LENGTH = 2;
    
    HoleWindage = 0.2;
    Protrusion = 0.1;
    NumSides = 3*2*4;
    
    $fn=NumSides;
    
    Sleeve = [18.5,22.0,14.0];
    Kerf = 1.0;
    
      difference() {
        tube(Sleeve[LENGTH],id=Sleeve[ID],od=Sleeve[OD],anchor=BOTTOM);
        for (a=[0,90])
          zrot(a)
            up(Sleeve[LENGTH]/4)
              cuboid([2*Sleeve[OD],Kerf,Sleeve[LENGTH]],anchor=BOTTOM);
      }
    
    

    That was easy …

  • 3D Printed Smashed Glass Coasters: Fragment Path Offsets, Simplified Version

    3D Printed Smashed Glass Coasters: Fragment Path Offsets, Simplified Version

    Rather than use Inkscape or LightBurn to generate all the offsets required to make a solid model, it’s easier to let OpenSCAD handle it:

    Printed Coaster Layout - 100 mm Set G - solid model
    Printed Coaster Layout – 100 mm Set G – solid model

    The overall process:

    • Pick some interesting fragments
    • Scan to get an image
    • Mark the fragments in GIMP
    • Create a suitable circumcircle in LightBurn
    • Use a nesting program like Deepnest to create a nice layout of the fragments within the circle
    • Create the perimeter path as an offset around all the fragments in LightBurn

    Because the fragments have irregular shapes and spacing, creating the perimeter path may also produce small snippets of orphaned geometry which must be manually selected and deleted. I also edit the path to remove very narrow channels between adjacent fragments.

    Which is why you can’t generate that path automatically:

    Printed Coaster Layout - 100 mm Set G - LightBurn perimeter geometry
    Printed Coaster Layout – 100 mm Set G – LightBurn perimeter geometry

    Because LightBurn doesn’t have the ability to name the various paths, the next step requires Inkscape. After importing the LightBurn paths saved as an SVG, group all the fragments and name the group Fragments, then name the perimeter path Perimeter:

    Printed Coaster Layout - 100 mm Set G - Inkscape layer and IDs
    Printed Coaster Layout – 100 mm Set G – Inkscape layer and IDs

    Inkscape still crashes unpredictably while doing what seems to be a simple process, which may be due to the tremendous number of points in the hand-traced fragment outlines. Unfortunately, simplifying the curves in either LightBurn or Inkscape tends to round off the extreme points and increases the likelihood of the fragment not fitting into its recess.

    OpenSCAD generates all the other features in the solid model with paths plucked from that file:

    include <BOSL2/std.scad>
    
    fn = "Printed Fragment Coaster - 100 mm Set G - Inkscape paths.svg";
    
    FragmentThick = 3.8;
    
    BaseThick = 1.0;
    RimHeight = 1.0;
    
    union() {
    
      linear_extrude(h=BaseThick)
        import(fn,id="Perimeter");
    
       color("Green")
      up(BaseThick)
        linear_extrude(h=FragmentThick)
          difference() {
            import(fn,id="Perimeter");
            offset(delta=0.2)
              import(fn,id="Fragments");
          }
    
      color("Red")
      up(BaseThick)
        linear_extrude(h=FragmentThick + RimHeight)
          difference() {
            offset(delta=2.5)
              import(fn,id="Fragments");
            offset(delta=1.2)
              import(fn,id="Fragments");
          }
    
    
    }
    

    The Perimeter path defines the overall shape of the coaster as a 1.0 mm thick slab, visible as the white-ish line around the edge and at the bottom of all the fragment recesses.

    Atop that, the green shape is the same Perimeter shape, with the Fragment shapes removed after the offset() operation enlarges them by 0.2 mm to ensure enough clearance.

    Finally, the red walls containing the epoxy above each fragment are 1.3 mm wide, the difference of the two offset() operations applied to the Fragments.

    Because the outer edge of the wall is 2.5 mm away from the edge of its fragment:

    • The Perimeter path must be offset at least 2.5 mm from the Fragments in LightBurn. I used 4.0 mm to produce a small lip around the outside edge of the coaster.
    • The fragment shapes must be placed at least 5.0 mm apart to prevent the walls from overlapping. I set Deepnest to exactly 5.0 mm spacing, but you can see a few places where the fragments come too close together. I think this happens due to an approximation deepnest uses while rotating the paths, but it may be better to manually adjust the errant fragments than increase the average space.

    While this still requires manually tracing the glass fragments and fiddling a bit with Inkscape, the overall process isn’t nearly as burdensome as getting all the offsets correct every time.

    However, some oddities remain. OpenSCAD produced this result during the first pass through the process for this coaster:

    Printed Coaster Layout - 100 mm Set G - spurious point
    Printed Coaster Layout – 100 mm Set G – spurious point

    As far as I can tell, the spurious point came from a numeric effect, because telling Inkscape to store only five decimal places in the SVG file reduced the spike to the small bump seen in the first picture. I cannot replicate that effect using the same files and have no explanation.

  • 3D Printed Smashed Glass Coasters: Fragment Path Offsets, Complicated Version

    3D Printed Smashed Glass Coasters: Fragment Path Offsets, Complicated Version

    This should have been trivially easy and turned into a nightmare.

    The problem to be solved is generating paths around fragments for the various recesses / reflectors / lips / rims / whatever. This clutter collector was a test piece:

    Smashed Glass Clutter Collector - overview
    Smashed Glass Clutter Collector – overview

    The corresponding paths:

    Printed Clutter Collector - Inkscape layers
    Printed Clutter Collector – Inkscape layers

    Which was how I convinced myself I didn’t need all those paths to make the thing, but that’s why it’s a test piece.

    Anyhow, Inkscape has a remarkably complex and fiddly way of generating precise offsets:

    • Select a path
    • Hit Ctrl-J to create a Dynamic Offset path
    • Drag the offset path away from the original in any direction for any distance
    • Hit Ctrl-Shift-x to fire up the XML editor (!)
    • Change the offset path’s inkscape:radius property to the desired offset

    During the course of working that out, I discovered Inkscape 1.4.2 is incredibly crashy when creating and dealing with offsets, to the point that I simply gave up trying to do that.

    LightBurn has no trouble creating a path at a specific offset from another path and can export the result as an SVG file. You then use Inkscape to set the path IDs so that OpenSCAD can import them by name for a specific use. Although Inkscape isn’t entirely stable doing even that seemingly trivial task, it’s usable.

    For reasons I do not profess to understand, setting the name of a path sometimes does not set its ID property, which is required by OpenSCAD to extract it from the SVG file. Instead, you must verify / set the ID using the path’s Object Properties window:

    Printed Clutter Collector - Inkscape path properties
    Printed Clutter Collector – Inkscape path properties

    I also set the Label property, because … why not?

    A top view shows how the various paths look in real life:

    Smashed Glass Clutter Collector - top view
    Smashed Glass Clutter Collector – top view

    The OpenSCAD program generating the solid model from those paths:

    include <BOSL2/std.scad>
    
    fn = "Printed Clutter Collector - Inkscape layers.svg";
    
    FragmentThick = 5.0;
    
    BaseThick = 1.0;
    RimHeight = 7.0;
    
    union() {
      linear_extrude(h=BaseThick)
        import(fn,id="Perimeter");
        
      linear_extrude(h=BaseThick + FragmentThick + RimHeight)
        difference() {
          import(fn,id="Perimeter");
          import(fn,id="Rim");
        }
    
      up(BaseThick - 0.05)
        linear_extrude(h=FragmentThick)
          difference() {
            import(fn,id="Perimeter");
            import(fn,id="Recess");
          }
    
    }
    

    Which becomes this:

    Printed Clutter Collector - solid model
    Printed Clutter Collector – solid model

    Save that, import it into PrusaSlicer, pick the filament, and print it out.

    While the printer buzzes away, use LightBurn to cut a shiny blue metallized paper reflector and a cork base using the appropriate paths; presumably you set those paths to LightBurn layers corresponding to the various materials. The Inkscape file has those paths with their names, because … why not?

    To assemble:

    • Cover the bottom of the recess with epoxy
    • Squish the reflector in place with epoxy oozing around it on all sides
    • Cover the reflector with epoxy
    • Squish the fragment atop the reflector with epoxy oozing around it on all sides
    • Fill the recess level with the lip inside the perimeter wall
    • Pop bubbles as needed
    • When it’s cured, stick the cork sheet on the bottom

    Note that the OpenSCAD program uses the path geometry without question, so it’s your responsibility to create them with the proper offsets and names.

    While all of that to-ing and fro-ing works, in the sense that I did make a rather nice clutter collector, it’s entirely too complicated and fiddly to be useful. Instead, I can now generate a coaster from just the fragment outlines and the coaster’s outer perimeter, a straightforward process which requires a bit more explanation.