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.

The New Hotness

  • HQ Sixteen: Grip Angle Caps

    The stock Handi-Quilter HQ Sixteen has serviceable black rubber caps covering the holes for the grips:

    HQ Sixteen - original front handlebar mount
    HQ Sixteen – original front handlebar mount

    Because we live in the future, I can do better than that:

    HQ Sixteen - grip cap left
    HQ Sixteen – grip cap left

    In truth, the plastic grip plug now sticks up into the hole just beyond the top setscrews, leaving not enough room for the black plugs, so I had to make new covers.

    They’re a multi-material print using white PETG to kinda-sorta match the machine and blue PETG-CF to match the other plastic parts. The colors in the solid model just distinguish the two materials:

    Handlebar Grip Mount - plug caps - solid model
    Handlebar Grip Mount – plug caps – solid model

    The white covers have recesses exactly fitting the text:

    Handlebar Grip Mount - cap text recess - solid model
    Handlebar Grip Mount – cap text recess – solid model

    In some cases that’s not needed, but I’m unsure how PrusaSlicer knows what I intend and chopping the text out was easy, so that’s how I did it.

    I did not realize applying a transformation, like translate() or BOSL2’s syntactic sugar left(), to both the cover and the text implicitly joins them into a single “object”, so the slicer can’t distinguish them as separate materials. As a result, the OpenSCAD code must move the pieces separately:

    if (Layout == "Covers") {
         left(Plug[OD]) GripCover(LEFT,"Cover");
         left(Plug[OD]) GripCover(LEFT,"Text");
    
         right(Plug[OD]) GripCover(RIGHT,"Cover");
         right(Plug[OD]) GripCover(RIGHT,"Text");
    }
    

    Which is awkward, but not insurmountable.

    Export the model as a 3mf file, import it into PrusaSlicer, and you get separate objects for the cover and the text. Assign different materials and slice to produce a multi-material result, with the Wipe Tower in the background:

    Handlebar Grip Mount - cap first layer - PrusaSlicer
    Handlebar Grip Mount – cap first layer – PrusaSlicer

    They must print face down to merge the two colors into a single flat surface with a nubbly texture from the steel sheet’s coating.

    Disks of adhesive sheet will eventually stick them atop the plugs, but for now they’re just dropped into the holes.

    The OpenSCAD source code as a GitHub Gist:

    // Handiquilter HQ Sixteen front handlebar grip angle mount
    // Ed Nisley – KE4ZNU
    // 2024-11-29
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build,Plug,Block,Covers,Cover]
    Material = "All"; // [All,Cover,Text]
    // Angle w.r.t. base
    GripAngle = 20; // [10:30]
    // Plug glued, not screwed
    PlugGlue = true;
    // Square nuts, not inserts
    SquareNuts = true;
    // Additional length of bottom
    AddLength = 0; // [0:20]
    // Separation in Show display
    Gap = 5; // [0:20]
    /* [Hidden] */
    HoleWindage = 0.1;
    Protrusion = 0.1;
    NumSides = 2*3*4;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    Grip = [19.7,22.4,20.0]; // (7/8)*INCH = 22.2 mm + roughness, LENGTH=OEM insertion depth
    GripRadius = Grip[OD]/2; // used everywhere
    Plug = [15.0,Grip[OD],45.0]; // inserts into handlebar base
    PlugRim = [Plug[ID],25.0,10.0]; // … sits against handlebar base
    BaseScrewPositions = [[11.0,12.0],[27.0,29.0]]; // setscrew offsets from rim top: side,rear
    BaseCutout = [Plug[OD]/2,Plug[ID],10]; // cable cutout into base
    BaseCutoutOffset = 18.0; // … centerline position w.r.t. rim
    WallThick = 7.0; // should at least fit insert length
    SupportSag = 0.4; // vertical sag over support structure
    MidLength = AddLength + 3.0; // total length allowing for grip tube stop
    TopOD = PlugRim[OD] + 2*WallThick;
    BotOD = Grip[OD] + 2*WallThick;
    BaseScrew = [4.0,4.8 + HoleWindage,1.0]; // HQ 10-32 screws, LENGTH=capture dent
    Insert = [5.4,6.0,6.0]; // M4 inserts in plug rim
    //Insert = [4.0,5.0,5.0]; // M4 inserts in plug rim
    Screw = [3.5,4.0,1]; // M4 screws through angle block to inserts
    ScrewHeadOD = 7.4 + 0.4; // M4 BHCS head + comfort
    SquareNut = [4.0,7.0,3.0 + 0.4]; // M4 square nut LENGTH + inset allowance
    NutInset = GripRadius – sqrt(pow(GripRadius,2) – pow(SquareNut[OD],2)/4);
    PinOD = 1.2; // plug reinforcing pins
    NumPins = 5;
    CoverThick = [3.5,9.5]; // low and high sides of grip covers
    CoverAngle = atan((CoverThick[1] – CoverThick[0])/Plug[OD]);
    LogoText = ["Sew","Fine"];
    LogoFont = "Fira Sans Condensed:style=SemiBold";
    LogoSize = 7.5;
    LogoColor = "Red";
    LogoThick = 0.8;
    //———-
    // Simulator for aluminum plug replacing handlebar in base
    module BasePlug() {
    difference() {
    union() {
    tube(Plug[LENGTH],(Plug[OD] – HoleWindage)/2,Plug[ID]/2,anchor=DOWN);
    tube(PlugRim[LENGTH],PlugRim[OD]/2,PlugRim[ID]/2,anchor=DOWN);
    }
    up(BaseCutoutOffset + PlugRim[LENGTH])
    left(Plug[OD]/4)
    resize(BaseCutout)
    yrot(90) zrot(180/8)
    cylinder(d=1,h=1,$fn=8,center=true);
    up(PlugRim[LENGTH])
    right(PlugRim[OD]/2 – 1.0)
    cube([2.0,1.0,1.0],center=true);
    for (i = [0:NumPins – 1])
    zrot(i*360/NumPins + 180/NumPins)
    down(Protrusion)
    right((Plug[OD] + Plug[ID])/4)
    zrot(180/6)
    cylinder(d=PinOD,h=2*PlugRim[LENGTH],$fn=6);
    for (k = [0:1]) // recesses in plug to capture base setscrews
    for (a = [0:1])
    up(PlugRim[LENGTH] + BaseScrewPositions[k][a])
    zrot(a*90)
    right(Plug[OD]/2)
    yrot(90) zrot(180/8)
    cylinder(d=BaseScrew[OD],h=2*BaseScrew[LENGTH],$fn=8,center=true);
    if (!PlugGlue)
    for (a = [0:1]) // inserts for angle block screws
    up(PlugRim[LENGTH]/2)
    zrot(a*90)
    yrot(90) zrot(180/8)
    cylinder(d=Insert[OD],h=2*PlugRim[OD],$fn=8,center=true);
    }
    }
    //———-
    // Block fitting against handlebar base with handlebar angle
    module AngleBlock() {
    difference() {
    hull() {
    up((TopOD/2)*sin(GripAngle))
    xrot(GripAngle)
    cylinder(d=TopOD,h=PlugRim[LENGTH],$fn=NumSides);
    for (a = [1:2:GripAngle+1])
    up((TopOD/2)*sin(a-1))
    hull() {
    xrot(a)
    cylinder(d=TopOD,h=0.1,$fn=NumSides);
    xrot(a-1)
    cylinder(d=TopOD,h=0.1,$fn=NumSides);
    }
    down(Grip[LENGTH] + MidLength)
    cylinder(d=(Grip[OD] + 2*WallThick),h=0.1,$fn=NumSides);
    }
    up((TopOD/2)*sin(GripAngle))
    xrot(GripAngle)
    down(SupportSag)
    cylinder(d=(PlugRim[OD] + HoleWindage),
    h=PlugRim[LENGTH] + SupportSag + Protrusion,
    $fn=NumSides);
    up((TopOD/2)*sin(GripAngle))
    sphere(d=PlugRim[ID],$fn=NumSides);
    cylinder(d=PlugRim[ID],h=(TopOD/2)*sin(GripAngle),$fn=NumSides);
    down(MidLength + Protrusion)
    cylinder(d=(Grip[ID] – 2.0),h=(MidLength + 2*Protrusion),$fn=NumSides);
    down(Grip[LENGTH] + MidLength + Protrusion)
    cylinder(d=(Grip[OD] + HoleWindage),h=(Grip[LENGTH] + Protrusion),$fn=NumSides);
    up((TopOD/2)*sin(GripAngle))
    xrot(GripAngle)
    up(PlugRim[LENGTH])
    right(PlugRim[OD]/2 + 0.9)
    cube([2.0,1.0,1.0],center=true);
    if (!PlugGlue) {
    for (a = [0:1])
    up((TopOD/2)*sin(GripAngle))
    xrot(GripAngle)
    up(PlugRim[LENGTH]/2)
    zrot(a*90)
    yrot(90) zrot(180/8)
    cylinder(d=Screw[OD],h=3*PlugRim[OD],$fn=8,center=true);
    for (a = [0:3])
    up((TopOD/2)*sin(GripAngle))
    xrot(GripAngle)
    up(PlugRim[LENGTH]/2)
    zrot(a*90)
    right(TopOD/2 – 2.0)
    yrot(90) zrot(180/8)
    cylinder(d=ScrewHeadOD,h=TopOD,$fn=8,center=false);
    }
    if (SquareNuts) {
    for (a = [0:1])
    for (k = [1,3])
    down(k*Grip[LENGTH]/4 + MidLength)
    zrot(a*90)
    right(BotOD/2)
    yrot(90) zrot(180/8)
    cylinder(d=SquareNut[ID],h=BotOD,$fn=8,center=true);
    for (a = [0:1])
    for (k = [1,3])
    down(k*Grip[LENGTH]/4 + MidLength)
    zrot(a*90)
    right(GripRadius + SquareNut[LENGTH]/2 – NutInset/2)
    yrot(90)
    cube([SquareNut[OD],SquareNut[OD],SquareNut[LENGTH] + NutInset],center=true);
    }
    else {
    for (a = [0:1])
    for (k = [1,3])
    down(k*Grip[LENGTH]/4 + MidLength)
    zrot(a*90)
    right(BotOD/2)
    yrot(90) zrot(180/8)
    cylinder(d=Insert[OD],h=BotOD,$fn=8,center=true);
    }
    }
    }
    //———-
    // Chip fitting against handlebar base matching top angle
    // Text will be invisible until sliced
    module GripCover(loc=LEFT,matl="Cover") {
    if (matl == "Text" || matl == "All")
    color(LogoColor)
    down(matl == "All" ? 0.01 : 0.0)
    text3d(LogoText[loc == LEFT ? 0 : 1],LogoThick,LogoSize,LogoFont,
    orient=DOWN,anchor=TOP,atype="ycenter");
    if (matl == "Cover" || matl == "All")
    difference() {
    intersection() {
    yrot(loc == RIGHT ? -CoverAngle : CoverAngle)
    cylinder(d=Plug[OD],h=(CoverThick[0] + CoverThick[1]),anchor=CENTER);
    cube(2*Plug[OD],anchor=BOTTOM);
    }
    text3d(LogoText[loc == LEFT ? 0 : 1],LogoThick,LogoSize,LogoFont,
    orient=DOWN,anchor=TOP,atype="ycenter");
    }
    }
    //———-
    // Build things
    if (Layout == "Cover") {
    GripCover(LEFT,Material);
    }
    if (Layout == "Covers") {
    left(Plug[OD]) GripCover(LEFT,"Cover");
    left(Plug[OD]) GripCover(LEFT,"Text");
    right(Plug[OD]) GripCover(RIGHT,"Cover");
    right(Plug[OD]) GripCover(RIGHT,"Text");
    }
    if (Layout == "Plug")
    BasePlug();
    if (Layout == "Block")
    AngleBlock();
    if (Layout == "Show") {
    up((TopOD/2)*sin(GripAngle) + Protrusion)
    xrot(GripAngle)
    up(Plug[LENGTH] + CoverThick[1] + Gap)
    yrot(180 + CoverAngle)
    GripCover(RIGHT,"All");
    up((TopOD/2)*sin(GripAngle) + Protrusion)
    xrot(GripAngle)
    up(Gap)
    color("Lime",0.75)
    BasePlug();
    render()
    difference() {
    AngleBlock();
    back(50) right(50)
    cube(100,center=true);
    }
    color("Silver",0.5)
    down(MidLength + Gap)
    tube(3*Grip[LENGTH],GripRadius,Grip[ID]/2,anchor=TOP);
    }
    if (Layout == "Build") {
    mirror_copy([1,0,0]) {
    right(BotOD) {
    up((TopOD/2)*sin(GripAngle) + PlugRim[LENGTH]*cos(GripAngle) + Protrusion)
    xrot(180 – GripAngle)
    AngleBlock();
    back(1.5*max(TopOD,BotOD))
    BasePlug();
    }
    }
    fwd(60) {
    left(Plug[OD]) GripCover(LEFT,"Cover");
    right(Plug[OD]) GripCover(RIGHT,"Cover");
    }
    fwd(60) {
    left(Plug[OD]) GripCover(LEFT,"Text");
    right(Plug[OD]) GripCover(RIGHT,"Text");
    }
    }