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.

Tag: M2

Using and tweaking a Makergear M2 3D printer

  • Photo Lamp Mount: Moah Plastic!

    One of the cold shoe mounts I made for the photo lamps cracked:

    Photo Lamp Mount - fractured
    Photo Lamp Mount – fractured

    It’s done in PETG with my more-or-less standard two perimeter threads and 15% 3D honeycomb infill, which is Good Enough™ for most of my parts. In this case, there’s obviously not nearly enough plastic in there!

    Redoing it with three perimeters and 50% infill should improve the situation, even though it looks identical on the outside:

    Photo Lamp Mount - reinstalled
    Photo Lamp Mount – reinstalled

    I didn’t replace the other mount. If it breaks, it’ll get the same 50% infill as this one. If this one breaks, I’ll try 75%.

    An easy fix!

  • CNC 3018XL: Adding Run-Hold Switches

    Although the bCNC GUI has conspicuous Run / Hold buttons, it’s easier to poke a physical switch when you really really need a pause in the action or have finished a (manual) tool change. Rather than the separate button box I built for the frameless MPCNC, I designed a chunky switch holder for the CNC 3018XL’s gantry plate:

    CNC 3018-Pro - Run Hold Switches - installed
    CNC 3018-Pro – Run Hold Switches – installed

    The original 15 mm screws were just slightly too short, so those are 20 mm stainless SHCS with washers.

    The switches come from a long-ago surplus deal and have internal green and red LEDs. Their transparent cap shows what might be white plastic underneath:

    CNC 3018-Pro - Run Hold Switches - top unlit
    CNC 3018-Pro – Run Hold Switches – top unlit

    I think you could pry the cap off and tuck a printed legend inside, but appropriate coloration should suffice:

    CNC 3018-Pro - Run Hold Switches - lit
    CNC 3018-Pro – Run Hold Switches – lit

    Making yellow from red and green LEDs always seems like magic; in these buttons, red + green produces a creamy white. Separately, the light looks like what you get from red & green LEDs.

    The solid model shows off the recesses around the LED caps, making their tops flush with the surface to prevent inadvertent pokery:

    Run Hold Switch Mount - Slic3r
    Run Hold Switch Mount – Slic3r

    The smaller square holes through the block may require a bit of filing, particularly in the slightly rounded corners common to 3D printing, to get a firm press fit on the switch body. The model now has slightly larger holes which may require a dab of epoxy.

    A multi-pack of RepRap-style printer wiring produced the cable, intended for a stepper motor and complete with a 4-pin Dupont socket housing installed on one end. I chopped the housing down to three pins, tucked the fourth wire into a single-pin housing, and plugged them into the CAMtool V3.3 board:

    CNC 3018-Pro - Run Hold Switches - CAMtool V3.3 header
    CNC 3018-Pro – Run Hold Switches – CAMtool V3.3 header

    The CAMtool schematic matches the default GRBL pinout, which comes as no surprise:

    CAMtool schematic - Start Hold pinout
    CAMtool schematic – Start Hold pinout

    The color code, such as it is:

    • Black = common
    • Red = +5 V
    • Green = Run / Start (to match the LED)
    • Blue = Hold (because it’s the only color left)

    The cable goes into 4 mm spiral wrap for protection & neatness, with the end hot-melt glued into the block:

    CNC 3018-Pro - Run Hold Switches - bottom
    CNC 3018-Pro – Run Hold Switches – bottom

    The model now includes the wiring channel between the two switches, which is so obviously necessary I can’t imagine why I didn’t include it. The recess on the top edge clears the leadscrew sticking slightly out of the gantry plate.

    The LEDs require ballast resistors: 120 Ω for red and 100 Ω for green, producing about 15 mA in each LED. Those are 1/8 W film resistors; I briefly considered SMD resistors, but came to my senses just in time.

    A layer of black duct tape finishes the bottom sufficiently for my simple needs.

    Note: the CAMtool board doesn’t have enough +5 V pins, so add a row of +5 V pins just below the standard header. If you’ve been following along, you needed them when you installed the home switches:

    3018 CNC CAMTool - Endstop power mod
    3018 CNC CAMTool – Endstop power mod

    A doodle giving relevant dimensions and layouts:

    Run Hold Switch Mount - Layout Doodles
    Run Hold Switch Mount – Layout Doodles

    I originally planned to mount the switches on the other gantry plate and sketched them accordingly, but (fortunately) realized the stepper motor was in the way before actually printing anything.

    The OpenSCAD source code as a GitHub Gist:

    // CNC 3018-Pro Run-Hold Switches
    // Ed Nisley – KE4ZNU – 2020-01
    Layout = "Build"; // [Show,Build,ProjectionX,ProjectionY,ProjectionZ,Block]
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    ID = 0;
    OD = 1;
    LENGTH = 2;
    inch = 25.4;
    //———————-
    // Dimensions
    RodScrewOffset = [22,0,-14.5]; // X=left edge, Y=dummy, Z=from top edge
    BeamScrewOffset = [50,0,-10];
    LeadScrewOffset = [RodScrewOffset.x,0,-45]; // may be off the bottom; include anyway
    LeadScrew = [8.0,10.0,5.0]; // ID=actual, OD=clearance, LENGTH=stick-out
    Screw = [5.0,10.0,6.0]; // M5 SHCS, OD=washer, LENGTH=washer+head
    ScrewSides = 8; // hole shape
    WallThick = 3.0; // minimum wall thickness
    FlangeThick = 5.0; // flange thickness
    Switch = [15.0 + 2*HoleWindage,15.0 + 2*HoleWindage,12.5]; // switch body
    SwitchCap = [17.5,17.5,12.0]; // … pushbutton
    SwitchClear = SwitchCap + [2*2.0,2*2.0,Screw[OD]/(2*cos(180/ScrewSides))];
    SwitchContacts = 5.0; // contacts below switch
    SwitchBase = SwitchContacts + Switch.z; // bottom to base of switch
    MountOffset = abs(RodScrewOffset.z) + SwitchClear.z; // top of switch mounting plate
    FrameWidth = 60.0; // CNC 3018-Pro upright
    FrameRadius = 10.0; // … front corner rounding
    CornerRadius = 5.0; // pretty part rounding
    CornerSquare = 10; // dummy for square corner
    MountOAL = [FrameWidth, // covers machine frame
    2*FlangeThick + 2*Screw[LENGTH] + SwitchClear.y, // clear screw heads
    MountOffset + Switch.z + SwitchContacts
    ];
    echo(str("MountOAL: ",MountOAL));
    SwitchOC = [MountOAL.x/2,FlangeThick + 2*Screw[LENGTH] + SwitchClear.y/2,0];
    CableOD = 5.0;
    NumSides = 2*3*4;
    Gap = 2.0; // between build layout parts
    //———————-
    // 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);
    }
    // Projections for intersections
    module ProjectionX() {
    sr = CornerSquare/2;
    rotate([0,90,0]) rotate([0,0,90])
    linear_extrude(height=FrameWidth,convexity=3)
    // mirror([1,0]) // mount on motor side of gantry
    union() {
    translate([0,-MountOAL.z])
    square([FlangeThick,MountOAL.z]);
    hull() {
    translate([MountOAL.y – CornerRadius,-MountOffset + SwitchCap.z – CornerRadius])
    circle(r=CornerRadius,$fn=NumSides);
    translate([sr,-MountOffset + SwitchCap.z – sr])
    square(CornerSquare,center=true);
    translate([sr,-MountOAL.z + sr])
    square(CornerSquare,center=true);
    translate([MountOAL.y – sr,-MountOAL.z + sr])
    square(CornerSquare,center=true);
    }
    }
    }
    module ProjectionY() {
    sr = CornerSquare/2;
    rotate([90,0,0])
    translate([0,0,-FrameWidth])
    difference() {
    linear_extrude(height=2*FrameWidth,convexity=3)
    hull() {
    translate([FrameRadius,-FrameRadius])
    circle(r=FrameRadius,$fn=NumSides);
    translate([FrameWidth – sr,-sr])
    square(CornerSquare,center=true);
    translate([sr,-MountOAL.z + sr])
    square(CornerSquare,center=true);
    translate([MountOAL.x – sr,-MountOAL.z + sr])
    square(CornerSquare,center=true);
    }
    translate([RodScrewOffset.x,RodScrewOffset.z,-Protrusion])
    rotate(180/ScrewSides) PolyCyl(Screw[ID],2*(FrameWidth + Protrusion),ScrewSides);
    for (j=[-FlangeThick,FrameWidth + FlangeThick])
    translate([RodScrewOffset.x,RodScrewOffset.z,j])
    rotate(180/ScrewSides) PolyCyl(Screw[OD],FrameWidth,ScrewSides);
    translate([BeamScrewOffset.x,BeamScrewOffset.z,-Protrusion])
    rotate(180/ScrewSides) PolyCyl(Screw[ID],2*(FrameWidth + Protrusion),ScrewSides);
    for (j=[-FlangeThick,FrameWidth + FlangeThick])
    translate([BeamScrewOffset.x,BeamScrewOffset.z,j])
    rotate(180/ScrewSides) PolyCyl(Screw[OD],FrameWidth,ScrewSides);
    translate([LeadScrewOffset.x,LeadScrewOffset.z,FrameWidth – LeadScrew[LENGTH]])
    rotate(180/ScrewSides) PolyCyl(LeadScrew[OD],2*LeadScrew[LENGTH],ScrewSides);
    }
    }
    module ProjectionZ() {
    translate([0,0,-MountOAL.z])
    // mirror([0,1]) // mount on motor side of gantry
    difference() {
    linear_extrude(height=MountOAL.z,convexity=3)
    difference() {
    square([MountOAL.x,MountOAL.y]);
    translate([SwitchOC.x/2,SwitchOC.y])
    square([Switch.x,Switch.y],center=true);
    translate([3*SwitchOC.x/2,SwitchOC.y])
    square([Switch.x,Switch.y],center=true);
    }
    for (i=[-1,1])
    translate([i*SwitchOC.x/2 + MountOAL.x/2,SwitchOC.y,SwitchBase + MountOAL.z/2])
    cube([SwitchClear.x,SwitchClear.y,MountOAL.z],center=true);
    translate([-Protrusion,SwitchOC.y – 2*CableOD – Switch.y/2,-Protrusion])
    cube([MountOAL.x + 2*Protrusion,CableOD,CableOD + Protrusion],center=false);
    for (i=[-1,1])
    translate([i*SwitchOC.x/2 + MountOAL.x/2,SwitchOC.y – SwitchCap.y/2,CableOD/2 – Protrusion])
    cube([CableOD,SwitchClear.y/2,CableOD + Protrusion],center=true);
    translate([SwitchOC.x/2,SwitchOC.y – CableOD/2,-Protrusion])
    cube([SwitchOC.x,CableOD,CableOD + Protrusion],center=false);
    }
    }
    module Block() {
    intersection() {
    ProjectionX();
    ProjectionY();
    ProjectionZ();
    }
    }
    //- Build things
    if (Layout == "ProjectionX")
    ProjectionX();
    if (Layout == "ProjectionY")
    ProjectionY();
    if (Layout == "ProjectionZ")
    ProjectionZ();
    if (Layout == "Block")
    Block();
    if (Layout == "Show") {
    translate([-MountOAL.x/2,-MountOAL.y/2,MountOAL.z]) {
    Block();
    translate([MountOAL.x/2 + SwitchOC.x/2,SwitchOC.y,SwitchCap.z/2 – MountOAL.z + SwitchBase + 0*Switch.z])
    color("Yellow",0.75)
    cube(SwitchCap,center=true);
    translate([MountOAL.x/2 – SwitchOC.x/2,SwitchOC.y,SwitchCap.z/2 – MountOAL.z + SwitchBase + 0*Switch.z])
    color("Green",0.75)
    cube(SwitchCap,center=true);
    }
    }
    if (Layout == "Build")
    translate([-MountOAL.x/2,-MountOAL.y/2,MountOAL.z])
    Block();

    It seems bCNC doesn’t update its “Restart Spindle” message after a tool change when you poke the green button (instead of the GUI button), but that’s definitely in the nature of fine tuning.

  • Google Pixel 3a Microscope Adapter

    Hand-holding my Google Pixel 3a phone over the microscope eyepiece worked well enough to justify building Yet Another Camera Adapter:

    Pixel 3a Microscope Adapter - in action
    Pixel 3a Microscope Adapter – in action

    The solid model looks about like you’d expect:

    Google Pixel 3a Zoom Microscope Mount - solid model - top
    Google Pixel 3a Zoom Microscope Mount – solid model – top

    The “camera” actually has the outside dimensions of a Spigen case, rather than the bare phone, because dropping a bare phone is never a good idea.

    The base plate pretty much fills the M2’s platform:

    Pixel 3a Microscope Adapter - M2 platform
    Pixel 3a Microscope Adapter – M2 platform

    I originally arranged the four corners around the plate to print everything in one go, but an estimated six hours of print time suggested doing the corners separately would maximize local happiness. Which it did, whew, even if the plate ran for a bit over 4-1/2 hours.

    The snout is a loose fit around the 5× widefield microscope eyepiece, with the difference made up in a wrap of black tape; it’s much easier to adjust the fit upward than to bore out the snout. An overwrap of tape secures the snout to the eyepiece, which I’ve dedicated to the cause; the scope normally rocks 10× widefield glass.

    The tapered hole exposes the phone’s fingerprint reader to simplify unlocking, should it shut down while I’m fiddling with something else.

    The microscope doesn’t fully illuminate the camera’s entrance pupil at minimum zoom, with 4.5× filling the screen and (mostly) eliminating the vignette. The corner blocks have oversize holes to allow aligning the camera lens axis over the microscope optical axis. The solid model incorporates Lessons Learned from the version you see here, because you (well, I) can’t measure the camera axis with respect to the outside dimensions accurately enough:

    Pixel 3a Microscope Adapter - installed - front
    Pixel 3a Microscope Adapter – installed – front

    Although it’s less unsteady than it looks, microscopy requires a gentle touch at the best of times. The adapter doesn’t add much wobble to the outcome:

    Pixel 3a Microscope Adapter - installed - side
    Pixel 3a Microscope Adapter – installed – side

    The field is about 14×19 mm with the camera at 4.5× and the microscope at minimum zoom:

    Pixel 3a Microscope Adapter - test image - min mag
    Pixel 3a Microscope Adapter – test image – min mag

    You can see a little darkening on the upper and lower right corners, so the phone’s still minutely leftward.

    The field is about 1.5×2 mm at full throttle:

    Pixel 3a Microscope Adapter - test image - max mag
    Pixel 3a Microscope Adapter – test image – max mag

    Color balance with the cold white LED ring isn’t the best, but it’s survivable. Mad props to OpenCamera for exposing All. The. Controls. you might possibly need.

    The OpenSCAD source code as a GitHub Gist:

    // Google Pixel 3a mount for stereo zoom microscope
    // Ed Nisley – KE4ZNU – 2019-12
    Layout = "Show"; // [Show,BuildAll,BuildBumpers,BuildPlate,DrillGuide,Phone,Plate,Bumper]
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    ID = 0;
    OD = 1;
    LENGTH = 2;
    inch = 25.4;
    //———————-
    // Dimensions
    Phone = [74.5,156.0,12.0]; // inside Spigen case
    PhoneRadii = [10.0,10.0,3.0]; // corner rounding, likewise
    LensOffset = [-17.0,-18.5,0]; // looking at phone screen, (-) sign = from right/top edge
    PrintReader = [0,Phone.y/2 – 44.0,0]; // fingerprint reader from center
    PrintReaderDia = [20.0,30.0,0]; // … hole for access
    Eyepiece = [11.5,28.0 + 0.50,27.0]; // ID = lens, OD includes clearance
    Insert = [3.0,4.5,4.0]; // M3 threaded brass insert
    Screw = [3.0,7.0,3.5]; // OD = washer, LENGTH = washer + head height
    WallThick = 3.0; // minimum wall thickness
    Bumper = [2*Screw[OD],20.0,Phone.z]; // bumper edge piece
    BumperOAL = Bumper.y + Bumper.x; // outside length for corner piece
    BumperRadius = 2.0;
    MinMargin = 1.2*Bumper.x; // at least this much extra plate for bumpers
    echo(str("MinMargin: ",MinMargin));
    Plate = [IntegerMultiple(Phone.x + 2*MinMargin,5.0),
    IntegerMultiple(Phone.y + 2*MinMargin,5.0),
    false ? 3*ThreadThick : max(Insert[LENGTH] + 2*ThreadThick,WallThick)];
    PlateRadius = 5.0;
    echo(str("Plate: ",Plate," radius: ",PlateRadius));
    EmbossDepth = 2*ThreadThick + Protrusion;
    DebossHeight = EmbossDepth;
    ScrewOffset = Bumper.x/2;
    ScrewAdjust = 1.5*Screw[ID];
    NumSides = 2*3*4;
    Gap = 2.0; // between build layout parts
    //———————-
    // 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);
    }
    // Basic shapes
    // Overall phone outline
    module Phone() {
    hull()
    for (i=[-1,1], j=[-1,1], k=[-1,1])
    translate([i*(Phone.x/2 – PhoneRadii.x),j*(Phone.y/2 – PhoneRadii.y),k*(Phone.z/2 – PhoneRadii.z)])
    resize(2*PhoneRadii)
    sphere(r=1,$fn=NumSides);
    }
    module Plate() {
    union() {
    difference() {
    union() {
    hull()
    for (i=[-1,1], j=[-1,1])
    translate([i*(Plate.x/2 – PlateRadius),j*(Plate.y/2 – PlateRadius),0])
    cylinder(r=PlateRadius,h=Plate.z,center=true,$fn=NumSides);
    translate([Phone.x/2,Phone.y/2,-Eyepiece[LENGTH]/3 + Plate.z/2] + LensOffset)
    cylinder(d=Eyepiece[OD] + 2*WallThick,h=Eyepiece[LENGTH]/3,
    center=false,$fn=NumSides);
    translate([Phone.x/2,Phone.y/2,-2*Eyepiece[LENGTH]/3 + Plate.z/2 + Protrusion] + LensOffset)
    cylinder(d1=Eyepiece[OD] + 10*ThreadThick,
    d2=Eyepiece[OD] + 2*WallThick,
    h=Eyepiece[LENGTH]/3,
    center=false,$fn=NumSides);
    }
    translate([Phone.x/2,Phone.y/2,-2*Eyepiece[LENGTH] + Plate.z/2 + Protrusion] + LensOffset)
    PolyCyl(Eyepiece[OD],2*Eyepiece[LENGTH],NumSides);
    translate(PrintReader + [0,0,-Plate.z/2 – Protrusion])
    cylinder(d1=PrintReaderDia[OD],d2=PrintReaderDia[ID],h=Plate.z + 2*Protrusion,$fn=NumSides);
    for (i=[-1,1], j=[-1,1])
    translate([i*(Phone.x/2 + Bumper.x/2),j*(Phone.y/2 – Bumper.y/2),-Plate.z])
    PolyCyl(Insert[OD],2*Plate.z,8);
    for (i=[-1,1], j=[-1,1])
    translate([i*(Phone.x/2 – Bumper.y/2),j*(Phone.y/2 + Bumper.x/2),-Plate.z])
    PolyCyl(Insert[OD],2*Plate.z,8);
    translate([0,-12,Plate.z/2]) // recess for legend
    cube([55,40,EmbossDepth],center=true);
    }
    translate([0,0,Plate.z/2 – EmbossDepth])
    linear_extrude(height=DebossHeight,convexity=20)
    text(text="Pixel 3a",size=6,spacing=1.20,
    font="Arial:style:Bold",halign="center",valign="center");
    translate([0,-15,Plate.z/2 – EmbossDepth])
    linear_extrude(height=DebossHeight,convexity=20)
    text(text="Ed Nisley",size=6,spacing=1.20,
    font="Arial:style:Bold",halign="center",valign="center");
    translate([0,-25,Plate.z/2 – EmbossDepth])
    linear_extrude(height=DebossHeight,convexity=20)
    text(text="softsolder.com",size=4,spacing=1.20,
    font="Arial:style:Bold",halign="center",valign="center");
    }
    }
    module BumperPiece() {
    difference() {
    translate([0,-BumperOAL/2 + Bumper.x,0])
    hull()
    for (i=[-1,1], j=[-1,1])
    translate([i*(Bumper.x/2 – BumperRadius),j*(BumperOAL/2 – BumperRadius),0])
    cylinder(r=BumperRadius,h=Bumper.z,center=true,$fn=NumSides);
    translate([0,-Bumper.y/2,-Bumper.z])
    PolyCyl(ScrewAdjust,2*Bumper.z,8);
    }
    }
    // Side bumpers, XY origin at inner corner
    module BumperCorner() {
    union() {
    translate([Bumper.x/2,0,0])
    BumperPiece();
    translate([0,Bumper.x/2,0])
    rotate(-90)
    BumperPiece();
    }
    }
    //- Build things
    if (Layout == "Phone")
    Phone();
    if (Layout == "Plate")
    Plate();
    if (Layout == "Bumper")
    BumperCorner();
    if (Layout == "Show") {
    color("LightBlue") Plate();
    for (i=[-1,1], j=[-1,1]) {
    a =
    i > 0 && j > 0 ? 0 :
    i < 0 && j > 0 ? 90 :
    i > 0 && j < 0 ? -90 :
    180
    ;
    translate([i*Phone.x/2,j*Phone.y/2,Plate.z/2 + Bumper.z/2])
    rotate(a)
    color("LightGreen") BumperCorner();
    translate([0,0,Phone.z/2 + Plate.z/2 + Protrusion])
    color("DarkGray",0.5) Phone();
    }
    }
    if (Layout == "BuildAll") {
    translate([0,0,Plate.z/2])
    rotate([0,180,0])
    Plate();
    for (i=[-1,1], j=[-1,1]) {
    a =
    i > 0 && j > 0 ? 0 :
    i < 0 && j > 0 ? 90 :
    i > 0 && j < 0 ? -90 :
    180
    ;
    translate([i*(Plate.x/2 + Gap),j*(Plate.y/2 + Gap),Bumper.z/2])
    rotate(a)
    BumperCorner();
    }
    }
    if (Layout == "BuildPlate") {
    translate([0,0,Plate.z/2])
    rotate([0,180,0])
    Plate();
    }
    if (Layout == "BuildBumpers") {
    for (i=[-1,1], j=[-1,1]) {
    a =
    i > 0 && j > 0 ? 180 :
    i < 0 && j > 0 ? -90 :
    i > 0 && j < 0 ? 90 :
    0
    ;
    translate([i*(Bumper.x + Gap),j*(Bumper.x + Gap),Bumper.z/2])
    rotate(a)
    BumperCorner();
    }
    }
    if (Layout == "DrillGuide") {
    projection(cut=true)
    Plate();
    }

  • Homage Tektronix Circuit Computer: Pen Plotter Version

    A reproduction circular slide rule from the mid-1960s may not be the cutting edge of consumer demand, but the pen version of a Tektronix Circuit Computer came out pretty well:

    Homage Tektronix Circuit Computer - green on white laminated
    Homage Tektronix Circuit Computer – green on white laminated

    A Bash script compiles the GCMC code with eight different parameter combinations to produce pairs of G-Code files to draw (“engrave” being aspirational) and cut (“mill”, likewise) the three decks and the cursor.

    The CNC 3018XL with a Pilot V5RT pen draws the deck scales on white paper:

    Pilot V5RT holder - installed
    Pilot V5RT holder – installed

    Better paper definitely produces better results, so I must rummage through the Big Box o’ Paper to see what lies within. Laminating the decks improves their durability and matches the original Tek surface finish.

    The MPCNC with a drag knife blade cuts through a laminated deck like butter:

    Tek CC - MPCNC drag knife
    Tek CC – MPCNC drag knife

    Setting the XY origin to dead center on each deck requires carefully calibrating the USB video camera, with the end result accurate to maybe ±0.1 mm around the entire perimeter. Both machines move equal linear distances along both axes, which was definitely comforting.

    Having made half a dozen cursors from various bits of acrylic, none of which look particularly good, demonstrates my engraving hand is too weak for a complete slide rule:

    Tek Circuit Computer - cursor hairline
    Tek Circuit Computer – cursor hairline

    With logarithmic scales in hand, however, adapting the GCMC source code to produce general-purpose circular slide rules with only two decks and smaller diameters may be the way to improve my engraving-fu, as a full-scale Tektronix Circuit Computer would chew up three square-foot plastic sheets.

    A general-purpose slide rule would need multi-color (well, at least bi-color) labels and digits for red “inverse” scales to remind you (well, me) they read backwards. Some slipsticks use left-slanting italics, left-pointing markers (“<2”), or other weirdness, but they’re all different.

    An early small-scale version engraved on ABS came out OK, modulo poor ink fill:

    Tek CC bottom - ABS 160g 2400mm-min
    Tek CC bottom – ABS 160g 2400mm-min

    Engraving the decks on hard drive platters doesn’t count:

    Tek CC - bottom deck - scaled to HD platter
    Tek CC – bottom deck – scaled to HD platter

    All in all, it’s been an interesting exercise and, as you may have guessed, will become a Digital Machinist column.

    The GCMC and Bash source code as a GitHub Gist:

    // Tektronix Circuit Computer Reproduction
    // Ed Nisley KE4ZNU – 2019-11
    //—–
    // Library routines
    include("tracepath.inc.gcmc");
    include("engrave.inc.gcmc");
    TekOD = to_mm(7.75in); // orginal Tek Circuit Computer diameter
    FALSE = 0;
    TRUE = 1;
    //—–
    // Command line parameters
    // -D various useful tidbits
    // add unit to speeds and depths: 2000mm / -3.00mm / etc
    if (!isdefined("BaseOD")) {
    BaseOD = TekOD;
    }
    comment("Base OD: ",BaseOD);
    SizeRatio = BaseOD / TekOD; // overall scaling for different base diameters
    comment(" scale factor: ",SizeRatio);
    if (!isdefined("SelectPart")) {
    SelectPart = "Bottom";
    }
    comment("Part: ",SelectPart);
    if (!isdefined("Operation")) {
    Operation = "Engrave";
    }
    comment("Operation: ",Operation);
    if (!isdefined("ScaleSpeed")) {
    ScaleSpeed = 2400mm;
    }
    if (!isdefined("TextSpeed")) {
    TextSpeed = 2400mm;
    }
    // Engraving & drag knife force is proportional to depth, but you must know the coefficent!
    if (!isdefined("EngraveZ")) {
    EngraveZ = -1.0mm;
    }
    if (!isdefined("KnifeZ")) {
    KnifeZ = -2.0mm;
    }
    if (!isdefined("KnifeSpeed")) {
    KnifeSpeed = 1000mm;
    }
    //—–
    // Define useful constants
    SafeZ = 10.00mm; // above all obstructions
    TravelZ = 1.00mm; // within engraving area
    //—–
    // Overall values
    ScaleHeight = to_inch(3.0/8.0) * SizeRatio; // scale-to-scale distance
    WindowHeight = ScaleHeight; // cutout window opening
    DeckBottomOD = BaseOD; // deck sizes depend on scale height
    DeckMiddleOD = DeckBottomOD – 2*ScaleHeight;
    DeckTopOD = DeckMiddleOD – 2*(ScaleHeight + WindowHeight);
    ScaleArc = 18deg; // angular length of one decade: +CCW
    ScaleExdent = 0.20; // log spacing at end of scales to identifiers
    Scale2Pi = log10(2*pi()) * ScaleArc; // angular offset for scales using 2*pi
    ScaleRT = log10(2.197225) * ScaleArc; // angular offset for risetime
    TauAngle = 150deg; // arbitrary offset to 1.0 on tau scales
    TitleAngle = -50deg; // … to Tek title, then +180deg to logo
    INWARD = -1; // text and tick alignment (used as integers)
    OUTWARD = 1;
    TEXT_LEFT = -1; // text justification
    TEXT_CENTERED = 0;
    TEXT_RIGHT = 1;
    TextFont = FONT_HSANS_1_RS;
    TitleTextSize = 3.1 * SizeRatio * [1.0mm,1.0mm];
    LegendTextSize = 1.8 * SizeRatio * [1.0mm,1.0mm];
    ScaleTextSize = 1.4 * SizeRatio * [1.0mm,1.0mm];
    //—-
    // Define tick layout for scales
    // Numeric values = scale position, tick length
    // These are not algorithmic!
    TickMajor = 3.2mm * SizeRatio; // length of tick marks
    TickMid = 1.9mm * SizeRatio;
    TickMinor = 1.2mm * SizeRatio;
    TickScaleNarrow = {
    [1.0,TickMajor],
    [1.1,TickMinor],[1.2,TickMinor],[1.3,TickMinor],[1.4,TickMinor],
    [1.5,TickMid],
    [1.6,TickMinor],[1.7,TickMinor],[1.8,TickMinor],[1.9,TickMinor],
    [2.0,TickMajor],
    [2.2,TickMinor],[2.4,TickMinor],[2.6,TickMinor],[2.8,TickMinor],
    [3.0,TickMajor],
    [3.2,TickMinor],[3.4,TickMinor],[3.6,TickMinor],[3.8,TickMinor],
    [4.0,TickMajor],
    [4.5,TickMinor],
    [5.0,TickMajor],
    [5.5,TickMinor],
    [6.0,TickMajor],
    [6.5,TickMinor],
    [7.0,TickMajor],
    [7.5,TickMinor],
    [8.0,TickMajor],
    [8.5,TickMinor],
    [9.0,TickMajor],
    [9.5,TickMinor]
    };
    TickScaleWide = {
    [1.0,TickMajor],
    [1.1,TickMinor],[1.2,TickMinor],[1.3,TickMinor],[1.4,TickMinor],
    [1.5,TickMid],
    [1.6,TickMinor],[1.7,TickMinor],[1.8,TickMinor],[1.9,TickMinor],
    [2.0,TickMajor],
    [2.1,TickMinor],[2.2,TickMinor],[2.3,TickMinor],[2.4,TickMinor],
    [2.5,TickMid],
    [2.6,TickMinor],[2.7,TickMinor],[2.8,TickMinor],[2.9,TickMinor],
    [3.0,TickMajor],
    [3.2,TickMinor],[3.4,TickMinor],[3.6,TickMinor],[3.8,TickMinor],
    [4.0,TickMajor],
    [4.2,TickMinor],[4.4,TickMinor],[4.6,TickMinor],[4.8,TickMinor],
    [5.0,TickMajor],
    [5.5,TickMinor],
    [6.0,TickMajor],
    [6.5,TickMinor],
    [7.0,TickMajor],
    [7.5,TickMinor],
    [8.0,TickMajor],
    [8.5,TickMinor],
    [9.0,TickMajor],
    [9.5,TickMinor]
    };
    TickLabels = [1,2,5]; // labels only these ticks, must be integers
    TickGap = 0.50 * ScaleTextSize.y; // gap between text and ticks
    PivotOD = 5.0mm; // center bolt OD
    Legend1 = "Ed Nisley – KE4ZNU";
    Legend2 = "softsolder.com";
    //—————————————————————————–
    // Text & Scale Engraving
    //—–
    // Write text on a radial line
    function RadialText(TextPath,CenterPt,Radius,Angle,Justify,Orient) {
    local pl = TextPath[-1].x; // path length
    local ji = (Justify == TEXT_LEFT) ? 0mm : // justification, assume OUTWARD
    (Justify == TEXT_CENTERED) ? -pl/2 :
    (Justify == TEXT_RIGHT) ? -pl :
    0mm;
    if (Orient == INWARD) {
    TextPath = rotate_xy(TextPath,180deg);
    ji = -ji;
    }
    TextPath += [Radius + ji,0mm];
    return rotate_xy(TextPath,Angle) + CenterPt;
    }
    //—–
    // Draw a radial legend
    // Offset in units of char height: 0 = baseline on radius, +/- = above/below
    function RadialLegend(Text,Center,Radius,Angle,Justify,Orient,Offset) {
    local tp = scale(typeset(Text,TextFont),LegendTextSize) + [0mm,Offset * LegendTextSize.y];
    local tpr = RadialText(tp,Center,Radius,Angle,Justify,Orient);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    }
    //—–
    // Bend text around an arc
    function ArcText(TextPath,CenterPt,Radius,BaseAngle,Justify,Orient) {
    local pl = TextPath[-1].x; // path length
    local c = 2*pi()*Radius;
    local ta = to_deg(360 * pl / c); // subtended angle
    local ja = (Justify == TEXT_LEFT ? 0deg : // assume OUTWARD
    (Justify == TEXT_CENTERED) ? -ta / 2 :
    (Justify == TEXT_RIGHT) ? -ta :
    0deg);
    ja = BaseAngle + Orient*ja;
    local ArcPath = {};
    local pt,r,a;
    foreach(TextPath; pt) {
    if (!isundef(pt.x) && !isundef(pt.y) && isundef(pt.z)) { // XY motion, no Z
    r = (Orient == OUTWARD) ? Radius – pt.y : Radius + pt.y;
    a = Orient * 360deg * (pt.x / c) + ja;
    ArcPath += {[r*cos(a) + CenterPt.x, r*sin(a) + CenterPt.y,-]};
    }
    elif (isundef(pt.x) && isundef(pt.y) && !isundef(pt.z)) { // no XY, Z up/down
    ArcPath += {pt};
    }
    else {
    error("ArcText – Point is not pure XY or pure Z: " + to_string(pt));
    }
    }
    return ArcPath;
    }
    //—–
    // Draw scale legend
    function ArcLegend(Text,Radius,Angle,Orient) {
    local tp = scale(typeset(Text,TextFont),LegendTextSize);
    local tpa = ArcText(tp,[0mm,0mm],Radius,Angle,TEXT_CENTERED,Orient);
    feedrate(TextSpeed);
    engrave(tpa,TravelZ,EngraveZ);
    }
    //—–
    // Draw a decade of ticks & labels
    // ArcLength > 0 = CCW, < 0 = CW
    // UnitOnly forces just the unit tick, so as to allow creating the last tick of the scale
    function DrawTicks(Radius,TickMarks,TickOrient,UnitAngle,ArcLength,Decade,LabelOrient,UnitOnly) {
    feedrate(ScaleSpeed);
    local a,r0,r1,p0,p1;
    if (Decade == 1 || UnitOnly) { // draw unit marker
    a = UnitAngle;
    r0 = Radius + TickOrient * (TickMajor + 2*TickGap + ScaleTextSize.y);
    p0 = r0 * [cos(a),sin(a)];
    r1 = Radius + TickOrient * (ScaleHeight – 2*TickGap);
    p1 = r1 * [cos(a),sin(a)];
    goto(p0);
    move([-,-,EngraveZ]);
    move(p1);
    goto([-,-,TravelZ]);
    }
    local ticklist = UnitOnly ? {TickMarks[0]} : TickMarks;
    local tick;
    foreach(ticklist; tick) {
    a = UnitAngle + ArcLength * log10(tick[0]);
    p0 = Radius * [cos(a), sin(a)];
    p1 = (Radius + TickOrient*tick[1]) * [cos(a), sin(a)];
    goto(p0);
    move([-,-,EngraveZ]);
    move(p1);
    goto([-,-,TravelZ]);
    }
    feedrate(TextSpeed); // draw scale values
    local lrad = Radius + TickOrient * (TickMajor + TickGap);
    if (TickOrient == INWARD) {
    if (LabelOrient == INWARD) {
    lrad -= ScaleTextSize.y; // inward ticks + inward labels = offset inward
    }
    }
    else {
    if (LabelOrient == OUTWARD) {
    lrad += ScaleTextSize.y; // outward ticks + outward labels = offset outward
    }
    }
    ticklist = UnitOnly ? [TickLabels[0]] : TickLabels;
    local ltext,lpath,tpa;
    foreach(ticklist; tick) {
    ltext = to_string(Decade * to_int(tick));
    lpath = scale(typeset(ltext,TextFont),ScaleTextSize);
    a = UnitAngle + ArcLength * log10(tick);
    tpa = ArcText(lpath,[0mm,0mm],lrad,a,TEXT_CENTERED,LabelOrient);
    engrave(tpa,TravelZ,EngraveZ);
    }
    }
    //—–
    // Mark key locations
    function MarkPivot() {
    comment("Mark center point");
    feedrate(ScaleSpeed);
    if (TRUE) {
    goto([-,-,SafeZ]);
    goto([PivotOD/2,0,-]);
    move([-,-,EngraveZ]);
    circle_cw([0,0]); // outline pivot
    move([-PivotOD/2,0,-]); // draw X line
    goto([-,-,TravelZ]);
    goto([0,PivotOD/2,-]);
    move([-,-,EngraveZ]);
    move ([0,-PivotOD/2,-]); // draw Y line
    goto([-,-,TravelZ]);
    }
    }
    //—–
    // Draw attribution
    function DrawAttribution(AttribRad) {
    comment("Attribution at: ",AttribRad);
    feedrate(TextSpeed);
    local tp,tpa;
    if (Legend1) {
    tp = scale(typeset(Legend1,TextFont),TitleTextSize);
    tpa = ArcText(tp,[0mm,0mm],AttribRad,0deg,TEXT_CENTERED,OUTWARD);
    feedrate(TextSpeed);
    engrave(tpa,TravelZ,EngraveZ);
    }
    if (Legend2) {
    tp = scale(typeset(Legend2,TextFont),TitleTextSize);
    tpa = ArcText(tp,[0mm,0mm],AttribRad,180deg,TEXT_CENTERED,OUTWARD);
    feedrate(TextSpeed);
    engrave(tpa,TravelZ,EngraveZ);
    }
    if (FALSE) { // test code to verify ArcText
    comment("ArcText test");
    ctr = [0mm,0mm];
    tp = scale(typeset("Right Inward",TextFont),ScaleTextSize);
    tpa = ArcText(tp,ctr,30mm,45deg,TEXT_RIGHT,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    tp = scale(typeset("Right Outward",TextFont),ScaleTextSize);
    tpa = ArcText(tp,ctr,30mm,45deg,TEXT_RIGHT,OUTWARD);
    engrave(tpa,TravelZ,EngraveZ);
    tp = scale(typeset("Center Inward",TextFont),ScaleTextSize);
    tpa = ArcText(tp,ctr,20mm,45deg,TEXT_CENTERED,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    tp = scale(typeset("Center Outward",TextFont),ScaleTextSize);
    tpa = ArcText(tp,ctr,20mm,45deg,TEXT_CENTERED,OUTWARD);
    engrave(tpa,TravelZ,EngraveZ);
    tp = scale(typeset("Left Inward",TextFont),ScaleTextSize);
    tpa = ArcText(tp,ctr,10mm,45deg,TEXT_LEFT,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    tp = scale(typeset("Left Outward",TextFont),ScaleTextSize);
    tpa = ArcText(tp,ctr,10mm,45deg,TEXT_LEFT,OUTWARD);
    engrave(tpa,TravelZ,EngraveZ);
    goto([0mm,0mm,-]);
    move([40mm,40mm,-]);
    }
    if (FALSE) { // test code to verify RadialText
    comment("RadialText test");
    ctr = [0mm,0mm];
    r = 20mm;
    a = 0deg;
    tp = scale(typeset("Left Inward",TextFont),LegendTextSize);
    tpr = RadialText(tp,ctr,r,a,TEXT_LEFT,INWARD);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    tp = scale(typeset("Left Outward",TextFont),LegendTextSize);
    tpr = RadialText(tp,ctr,r,a,TEXT_LEFT,OUTWARD);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    a = 90deg;
    tp = scale(typeset("Right Inward",TextFont),LegendTextSize);
    tpr = RadialText(tp,ctr,r,a,TEXT_RIGHT,INWARD);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    tp = scale(typeset("Right Outward",TextFont),LegendTextSize);
    tpr = RadialText(tp,ctr,r,a,TEXT_RIGHT,OUTWARD);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    a = 180deg;
    tp = scale(typeset("Center Inward",TextFont),LegendTextSize);
    tpr = RadialText(tp,ctr,r,a,TEXT_CENTERED,INWARD);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    tp = scale(typeset("Center Outward",TextFont),LegendTextSize);
    tpr = RadialText(tp,ctr,r,a,TEXT_CENTERED,OUTWARD);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    a = 270deg;
    RadialLegend("Offset to radius",ctr,r,a,TEXT_CENTERED,INWARD,-0.5);
    goto(ctr);
    move([0,-2*r,EngraveZ]);
    goto([r,0mm,-]);
    circle_cw(ctr);
    }
    }
    //—————————————————————————–
    // Deck Engraving
    //———-
    // Engrave bottom deck
    function EngraveBottom() {
    // Mark center pivot
    MarkPivot();
    comment("Inductance scale");
    Radius = DeckRad – ScaleHeight;
    MinLog = -9;
    MaxLog = 6;
    Arc = -ScaleArc;
    dec = 1;
    offset = 0deg;
    for (logval = MinLog; logval < MaxLog; logval++) {
    a = offset + logval * Arc;
    DrawTicks(Radius,TickScaleNarrow,OUTWARD,a,Arc,dec,INWARD,FALSE);
    dec = (dec == 100) ? 1 : 10 * dec;
    }
    a = offset + MaxLog * Arc;
    DrawTicks(Radius,TickScaleNarrow,OUTWARD,a,Arc,1000,INWARD,TRUE);
    r = Radius + TickMajor + 2*TickGap + LegendTextSize.y;
    logval = MinLog + 1.5;
    a = offset + logval * Arc;
    ArcLegend("nH – nanohenry x10^-9",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("μH – microhenry x10^-6",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("mH – millihenry x10^-3",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("H – henry",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("kH – kilohenry x10^3",r,a,INWARD);
    r = Radius + TickMajor + TickGap;
    logval = MinLog – ScaleExdent; // scale identifiers
    a = offset + logval * Arc;
    tp = scale(typeset("L Scale →",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_RIGHT,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    logval = MaxLog + ScaleExdent;
    a = offset + logval * Arc;
    tp = scale(typeset("← L Scale",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_LEFT,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    comment("Inductive frequency scale");
    Radius = DeckRad – 2*ScaleHeight;
    MinLog = 0;
    MaxLog = 9;
    Arc = 2*ScaleArc; // double-length scale for square roots
    dec = 1;
    offset = -(18 * ScaleArc – Scale2Pi); // using 18 degree arc length
    for (logval = MinLog; logval < MaxLog; logval++) {
    a = offset + logval * Arc;
    DrawTicks(Radius,TickScaleWide,OUTWARD,a,Arc,dec,OUTWARD,FALSE);
    dec = (dec == 100) ? 1 : 10 * dec;
    }
    a = offset + MaxLog * Arc;
    DrawTicks(Radius,TickScaleWide,OUTWARD,a,Arc,1000,OUTWARD,TRUE);
    feedrate(TextSpeed); // draw prefix legends
    r = Radius + TickMajor + 2*TickGap + 2*LegendTextSize.y;
    logval = MinLog + 0.5;
    for (i = 0; i < 3; i++) {
    a = offset + (i + logval) * Arc;
    ArcLegend("Hz – hertz",r,a,OUTWARD);
    }
    for (i = 3; i < 6; i++) {
    a = offset + (i + logval) * Arc;
    ArcLegend("kHz – kilohertz x10^3",r,a,OUTWARD);
    }
    for (i = 6; i < 9; i++) {
    a = offset + (i + logval) * Arc;
    ArcLegend("MHz – megahertz x10^6",r,a,OUTWARD);
    }
    r = Radius + TickMajor + TickGap + LegendTextSize.y;
    logval = MinLog – 0.5; // scale identifier
    a = offset + logval * Arc;
    ArcLegend("←——- FL Scale ——-→",r,a,OUTWARD);
    comment("Inductive TC / Risetime scale");
    Radius = DeckRad – 3*ScaleHeight;
    MinLog = -12;
    MaxLog = 3;
    Arc = -ScaleArc;
    dec = 1;
    offset = -TauAngle;
    for (logval = MinLog; logval < MaxLog; logval++) {
    a = offset + logval * Arc;
    DrawTicks(Radius,TickScaleNarrow,OUTWARD,a,Arc,dec,INWARD,FALSE);
    dec = (dec == 100) ? 1 : 10 * dec;
    }
    a = offset + MaxLog * Arc;
    DrawTicks(Radius,TickScaleNarrow,OUTWARD,a,Arc,1000,INWARD,TRUE);
    feedrate(TextSpeed); // prefix legends
    r = Radius + TickMajor + 2*TickGap + LegendTextSize.y;
    logval = MinLog + 1.5;
    a = offset + logval * Arc;
    ArcLegend("ps – picosecond x10^-12",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("ns – nanosecond x10^-9",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("μs – microsecond x10^-6",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("ms – millisecond x10^-3",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("s – second",r,a,INWARD);
    r = Radius + TickMajor + TickGap;
    logval = MinLog – ScaleExdent; // scale identifiers
    a = offset + logval * Arc;
    tp = scale(typeset("τL Scale →",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_RIGHT,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    logval = MaxLog + ScaleExdent;
    a = offset + logval * Arc;
    tp = scale(typeset("← τL Scale",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_LEFT,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    //—–
    // Add construction notes
    comment("Attribution begins");
    r = DeckTopOD/2 – 2*ScaleHeight – WindowHeight;
    DrawAttribution(r);
    if (FALSE) {
    t = "Disk OD: " + to_string(DeckBottomOD) + " " +
    to_string(DeckMiddleOD) + " " +
    to_string(DeckTopOD) + " mm";
    tp = scale(typeset(t,TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,90deg,TEXT_CENTERED,OUTWARD);
    engrave(tpa,TravelZ,EngraveZ);
    }
    goto([-,-,SafeZ]); // done, so get out of the way
    goto([0,0,-]);
    comment("Bottom deck ends");
    }
    //———-
    // Engrave middle deck
    function EngraveMiddle() {
    // Mark center pivot
    MarkPivot();
    comment("Capacitance scale");
    Radius = DeckRad;
    MinLog = -15;
    MaxLog = 0;
    Arc = ScaleArc;
    dec = 1;
    offset = 0deg;
    for (logval = MinLog; logval < MaxLog; logval++) {
    a = offset + logval * Arc;
    DrawTicks(Radius,TickScaleNarrow,INWARD,a,Arc,dec,INWARD,FALSE);
    dec = (dec == 100) ? 1 : 10 * dec;
    }
    a = offset + MaxLog * Arc;
    DrawTicks(Radius,TickScaleNarrow,INWARD,a,Arc,1000,INWARD,TRUE);
    r = Radius – ScaleHeight + TickGap;
    logval = MinLog + 1.5;
    a = offset + logval * Arc;
    ArcLegend("fF – femtofarad x10^-15",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("pF – picofarad x10^-12",r,a,INWARD);
    logval += 2.5; // offset for L/R window;
    a = offset + logval * Arc;
    ArcLegend("nF – nanofarad x10^-9",r,a,INWARD);
    logval += 4; // … likewise
    a = offset + logval * Arc;
    ArcLegend("μF – microfarad x10^-6",r,a,INWARD);
    logval += 2.5; // … restore normal spacing
    a = offset + logval * Arc;
    ArcLegend("mF – millifarad x10^-3",r,a,INWARD);
    r = Radius – ScaleHeight – TickGap – LegendTextSize.y; // into blank space
    logval = MinLog; // scale identifiers
    a = offset + logval * Arc;
    tp = scale(typeset("←— C Scale",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_RIGHT,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    logval = MinLog + 6;
    a = offset + logval * Arc;
    tp = scale(typeset("←— C Scale —→",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_CENTERED,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    logval = MaxLog;
    a = offset + logval * Arc;
    tp = scale(typeset("C Scale —→",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_LEFT,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    comment("Capacitive TC / risetime scale");
    Radius = DeckRad – 4*ScaleHeight;
    MinLog = -12;
    MaxLog = 3;
    Arc = ScaleArc;
    dec = 1;
    offset = 3 * ScaleArc;
    for (logval = MinLog; logval < MaxLog; logval++) {
    a = offset + logval * Arc;
    DrawTicks(Radius,TickScaleNarrow,OUTWARD,a,Arc,dec,INWARD,FALSE);
    dec = (dec == 100) ? 1 : 10 * dec;
    }
    a = offset + MaxLog * Arc;
    DrawTicks(Radius,TickScaleNarrow,OUTWARD,a,Arc,1000,INWARD,TRUE);
    r = Radius + TickMajor + 2*TickGap + LegendTextSize.y;
    logval = MinLog + 1.5;
    a = offset + logval * Arc;
    ArcLegend("ps – picosecond x10^-12",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("ns – nanosecond x10^-9",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("μs – microsecond x10^-6",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("ms – millisecond x10^-3",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("s – second",r,a,INWARD);
    r = Radius + TickMajor + TickGap;
    logval = MinLog – ScaleExdent; // scale identifiers
    a = offset + logval * Arc;
    tp = scale(typeset("← τC Scale",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_LEFT,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    logval = MaxLog + ScaleExdent;
    a = offset + logval * Arc;
    tp = scale(typeset("τC Scale →",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_RIGHT,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    logval = MinLog – 2.5;
    a = offset + logval * Arc;
    tp = scale(typeset("←— τC Scale —→",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_CENTERED,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    comment("Inductive frequency scale legend");
    r = DeckRad – ScaleHeight – ScaleTextSize.y;
    a = 58deg; // arbitrary text placement
    ArcLegend("FL Scale",r,a,OUTWARD);
    comment("Index for resonance calculations");
    Index = -(18*ScaleArc + Scale2Pi); // negative to read reciprocal of product
    r = DeckRad – 1.5*ScaleHeight + 0.5*LegendTextSize.y;
    ArcLegend("Frequency",r,Index,OUTWARD);
    r = DeckRad – ScaleHeight – LegendTextSize.y;
    ArcLegend("⇑",(r – TickGap),Index,INWARD);
    r = DeckRad – 2*ScaleHeight + LegendTextSize.y;
    ArcLegend("⇑",(r + TickGap),Index,OUTWARD);
    r0 = DeckRad – ScaleHeight;
    r1 = r0 – TickMajor;
    goto(r0 * [cos(Index),sin(Index)]);
    move([-,-,EngraveZ]);
    move(r1 * [cos(Index),sin(Index)]);
    goto([-,-,TravelZ]);
    r0 = DeckRad – 2*ScaleHeight;
    r1 = r0 + TickMajor;
    goto(r0 * [cos(Index),sin(Index)]);
    move([-,-,EngraveZ]);
    move(r1 * [cos(Index),sin(Index)]);
    goto([-,-,TravelZ]);
    //—–
    // Draw the attribution
    comment("Attribution begins");
    r = DeckTopOD/2 – 2*ScaleHeight – WindowHeight;
    DrawAttribution(r);
    goto([-,-,SafeZ]); // done, so get out of the way
    goto([0,0,-]);
    comment("Middle deck ends");
    }
    //———-
    // Engrave top deck
    function EngraveTop() {
    // Mark center pivot
    MarkPivot();
    comment("Resistance scale");
    Radius = DeckRad;
    MinLog = -1;
    MaxLog = 8;
    Arc = -ScaleArc;
    dec = 100;
    offset = 0deg;
    for (logval = MinLog; logval < MaxLog; logval++) {
    a = offset + logval * Arc;
    DrawTicks(Radius,TickScaleNarrow,INWARD,a,Arc,dec,INWARD,FALSE);
    dec = (dec == 100) ? 1 : 10 * dec;
    }
    a = offset + MaxLog * Arc;
    DrawTicks(Radius,TickScaleNarrow,INWARD,a,Arc,100,INWARD,TRUE);
    r = Radius – ScaleHeight + TickGap;
    logval = MinLog + 0.5;
    a = offset + logval * Arc;
    ArcLegend("mΩ – milliohm",r,a,INWARD);
    logval = MinLog + 2.5;
    a = offset + logval * Arc;
    ArcLegend("Ω – ohm",r,a,INWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("kΩ – kilohm x10^3",r,a,INWARD);
    logval = MaxLog – 1;
    a = offset + logval * Arc;
    ArcLegend("MΩ – megohm x10^6",r,a,INWARD);
    r = Radius – ScaleHeight – TickGap – LegendTextSize.y;
    logval = MinLog + 4;
    a = offset + logval * Arc;
    ArcLegend("←— R XC XL Scale —→",r,a,INWARD);
    comment("Capacitive frequency scale");
    Radius = DeckRad;
    MinLog = 0;
    MaxLog = 9;
    Arc = ScaleArc;
    dec = 1;
    offset = 18 * -ScaleArc;
    for (logval = MinLog; logval < MaxLog; logval++) {
    a = offset + logval * Arc;
    DrawTicks(Radius,TickScaleNarrow,INWARD,a,Arc,dec,OUTWARD,FALSE);
    dec = (dec == 100) ? 1 : 10 * dec;
    }
    a = offset + MaxLog * Arc;
    DrawTicks(Radius,TickScaleNarrow,INWARD,a,Arc,1000,OUTWARD,TRUE);
    r = Radius – (TickMajor + 2*TickGap + LegendTextSize.y);
    logval = MinLog + 1.5;
    a = offset + logval * Arc;
    ArcLegend("Hz – hertz",r,a,OUTWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("kHz – kilohertz x10^3",r,a,OUTWARD);
    logval += 3;
    a = offset + logval * Arc;
    ArcLegend("MHz – megahertz x10^6",r,a,OUTWARD);
    r = Radius – ScaleHeight – TickGap – LegendTextSize.y;
    logval = MaxLog – 3;
    a = offset + logval * Arc;
    ArcLegend("←— FC Scale —→",r,a,OUTWARD);
    comment("RC Circuit Pointers");
    local ctr = [0mm,0mm];
    r0 = DeckRad – 2*ScaleHeight;
    r1 = r0 – ScaleHeight;
    a = -(17 * ScaleArc);
    goto(r0 * [cos(a),sin(a)]);
    move([-,-,EngraveZ]);
    move(r1 * [cos(a),sin(a)]);
    goto([-,-,TravelZ]);
    ArcLegend("⇓",(r0 – TickGap),a,OUTWARD);
    RadialLegend(" Time Constant",ctr,r1,a,TEXT_LEFT,INWARD,-0.5);
    a += ScaleRT;
    goto(r0 * [cos(a),sin(a)]);
    move([-,-,EngraveZ]);
    move(r1 * [cos(a),sin(a)]);
    goto([-,-,TravelZ]);
    ArcLegend("⇓",(r0 – TickGap),a,OUTWARD);
    RadialLegend(" Risetime",ctr,r1,a,TEXT_LEFT,INWARD,-0.5);
    a -= ScaleRT/2;
    RadialLegend(" RC",ctr,r0 – 2*ScaleTextSize.y,a,TEXT_LEFT,INWARD,-0.5);
    comment("L/R Circuit Pointers");
    r0 = DeckRad;
    r1 = r0 – ScaleHeight;
    a = -TauAngle;
    goto(r0 * [cos(a),sin(a)]);
    move([-,-,EngraveZ]);
    move(r1 * [cos(a),sin(a)]);
    goto([-,-,TravelZ]);
    ArcLegend("⇓",(r0 – TickGap),a,OUTWARD);
    RadialLegend("Time Constant ",ctr,r1,a,TEXT_RIGHT,OUTWARD,-0.5);
    a -= ScaleRT;
    goto(r0 * [cos(a),sin(a)]);
    move([-,-,EngraveZ]);
    move(r1 * [cos(a),sin(a)]);
    goto([-,-,TravelZ]);
    ArcLegend("⇓",(r0 – TickGap),a,OUTWARD);
    RadialLegend("Risetime ",ctr,r1,a,TEXT_RIGHT,OUTWARD,-0.5);
    a += ScaleRT/2;
    RadialLegend("L/R ",ctr,r0 – 2*ScaleTextSize.y,a,TEXT_RIGHT,OUTWARD,-0.5);
    comment("Title and logo");
    feedrate(TextSpeed);
    r = 0.65*DeckRad;
    tp = scale(typeset("Homage",TextFont),TitleTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,TitleAngle,TEXT_CENTERED,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    r -= 1.5*TitleTextSize.y;
    tp = scale(typeset("Tektronix",TextFont),TitleTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,TitleAngle,TEXT_CENTERED,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    r -= 1.5*TitleTextSize.y;
    tp = scale(typeset("Circuit Computer",TextFont),TitleTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,TitleAngle,TEXT_CENTERED,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    r -= 1.5*TitleTextSize.y;
    if (TRUE) {
    tp = scale(typeset("TEK 003-023",TextFont),LegendTextSize);
    }
    else {
    tp = scale(typeset("https://vintagetek.org/tektronix-circuit-computer/&quot;,TextFont),LegendTextSize);
    }
    tpa = ArcText(tp,[0mm,0mm],r,TitleAngle,TEXT_CENTERED,INWARD);
    engrave(tpa,TravelZ,EngraveZ);
    r = 0.3*DeckRad;
    a = TitleAngle + 180deg;
    tp = scale(typeset("Ed Nisley",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_CENTERED,OUTWARD);
    engrave(tpa,TravelZ,EngraveZ);
    r += 1.5*TitleTextSize.y;
    tp = scale(typeset("KE4ZNU",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_CENTERED,OUTWARD);
    engrave(tpa,TravelZ,EngraveZ);
    r += 1.5*TitleTextSize.y;
    tp = scale(typeset("softsolder.com",TextFont),LegendTextSize);
    tpa = ArcText(tp,[0mm,0mm],r,a,TEXT_CENTERED,OUTWARD);
    engrave(tpa,TravelZ,EngraveZ);
    goto([-,-,SafeZ]); // done, so get out of the way
    goto([0,0,-]);
    comment("Top deck ends");
    }
    //———-
    // Engrave cursor hairline
    function EngraveCursor() {
    // Mark center pivot
    MarkPivot();
    comment("Cursor hairline");
    feedrate(ScaleSpeed);
    goto([-,-,TravelZ]);
    repeat(2) {
    goto([DeckTopOD/2 – 2.25*ScaleHeight,0,-]); // slight overlap on arrows
    move([-,-,EngraveZ]);
    move([DeckBottomOD/2 + ScaleHeight,0,-]);
    goto([-,-,TravelZ]);
    }
    goto([-,-,SafeZ]); // done, so get out of the way
    goto([0,0,-]);
    }
    //—————————————————————————–
    // Deck milling
    // Assumes adhesive clamping to avoid protrusions above work area
    //—–
    // Bottom deck
    function MillBottom() {
    comment("Mill Bottom");
    feedrate(KnifeSpeed);
    goto([-,-,TravelZ]);
    local r = PivotOD/2;
    goto([0,r,-]); // entry move to align knife
    arc_cw([r,0,0],r); // blade enters surface
    move([-,-,KnifeZ]); // apply cutting force
    circle_cw([0,0]);
    arc_cw([0,-r],r); // cut past entry point
    goto([-,-,TravelZ]);
    r = DeckRad;
    local a = 5deg;
    local p0 = r * [cos(a),sin(a),-]; // entry point
    local p1 = r * [cos(-a),sin(-a),-]; // exit point
    goto(p0);
    arc_cw([r,0,0],r); // blade enters surface
    move([-,-,KnifeZ]); // apply cutting force
    circle_cw([0,0]); // cut circle
    arc_cw(p1,r); // cut past entry point
    goto([-,-,TravelZ]);
    goto([0,0,-]);
    goto([-,-,SafeZ]);
    }
    //—–
    // Middle deck
    function MillMiddle() {
    FLNotchArc = 85deg; // width exposing FL scale
    FLRampArc = 7deg; // … width of entry & exit ramps
    FLNotchOffset = 2deg; // … start angle from 0°
    comment("Mill Middle");
    feedrate(KnifeSpeed);
    goto([-,-,TravelZ]);
    local r = PivotOD/2;
    goto([0,r,-]); // entry move to align knife
    arc_cw([r,0,0],r); // blade enters surface
    move([-,-,KnifeZ]); // apply cutting force
    circle_cw([0,0]);
    arc_cw([0,-r],r); // cut past entry point
    goto([-,-,TravelZ]);
    // FL scale notch
    local r0 = DeckRad;
    local a0 = FLNotchOffset; // end of notch ramp
    local p0 = r0 * [cos(a0),sin(a0),-];
    local a1 = a0 + FLNotchArc; // start of notch ramp
    local p1 = r0 * [cos(a1),sin(a1),-];
    goto(p0);
    arc_cw([r0,0,0],r0); // blade enters surface
    move([-,-,KnifeZ]); // apply cutting force
    arc_cw(p1,-r0); // largest arc to start of notch
    local r1 = r0 – ScaleHeight;
    local a3 = a1 – FLRampArc; // start of notch base
    local p3 = r1 * [cos(a3),sin(a3),-];
    local a4 = a0 + FLRampArc; // end of notch base
    local p4 = r1 * [cos(a4),sin(a4),-];
    move(p3);
    arc_cw(p4,r1); // smallest arc on notch base
    move(p0); // end of notch ramp
    arc_cw([r0,0,-],r0); // round off corner
    local p5 = r0 * [cos(-a0),sin(-a0),-]; // small overtravel past entry point
    arc_cw(p5,r0);
    goto([-,-,TravelZ]);
    // L/R τ and RT Scale window
    local WindowArc = 39deg;
    ac = -6 * ScaleArc; // center of window arc
    r0 = DeckRad – ScaleHeight; // outer
    r1 = DeckRad – 2 * ScaleHeight; // inner
    aw = WindowArc – to_deg(atan(ScaleHeight,(r0 + r1)/2)); // window arc minus endcaps
    p0 = r0 * [cos(ac + aw/2),sin(ac + aw/2),-]; // endcap entry & exit
    p1 = r0 * [cos(ac – aw/2),sin(ac – aw/2),-];
    p2 = r1 * [cos(ac – aw/2),sin(ac – aw/2),-];
    p3 = r1 * [cos(ac + aw/2),sin(ac + aw/2),-];
    goto(p3); // cut entry point
    arc_cw(p0 +| [-,-,0],ScaleHeight/2); // blade enters surface
    move([-,-,KnifeZ]); // apply pressure
    arc_cw(p1,r0); // smallest arc
    arc_cw(p2,ScaleHeight/2); // half a circle
    arc_ccw(p3,r1);
    arc_cw(p0,ScaleHeight/2);
    arc_cw(p1 +| [-,-,TravelZ],r0); // exit from cut
    goto([0,0,-]);
    goto([-,-,SafeZ]);
    }
    //—–
    // Top deck
    function MillTop() {
    comment("Mill Top");
    feedrate(KnifeSpeed);
    goto([-,-,TravelZ]);
    local r = PivotOD/2;
    goto([0,r,-]); // entry move to align knife
    arc_cw([r,0,0],r); // blade enters surface
    move([-,-,KnifeZ]); // apply cutting force
    circle_cw([0,0]);
    arc_cw([0,-r],r); // cut past entry point
    goto([-,-,TravelZ]);
    r = DeckRad;
    local a = 5deg;
    local p0 = r * [cos(a),sin(a),-]; // entry point
    local p1 = r * [cos(-a),sin(-a),-]; // exit point
    goto(p0);
    arc_cw([r,0,0],r); // blade enters surface
    move([-,-,KnifeZ]); // apply cutting force
    circle_cw([0,0]); // cut circle
    arc_cw(p1,r); // cut past entry point
    goto([-,-,TravelZ]);
    // RC τ and RT Scale window
    local WindowArc = 54deg;
    local ac = -17 * ScaleArc + ScaleRT/2; // center of window arc
    local r0 = DeckRad – ScaleHeight; // outer
    local r1 = DeckRad – 2 * ScaleHeight; // inner
    local aw = WindowArc – to_deg(atan(ScaleHeight,(r0 + r1)/2)); // window arc minus endcaps
    p0 = r0 * [cos(ac + aw/2),sin(ac + aw/2),-];
    p1 = r0 * [cos(ac – aw/2),sin(ac – aw/2),-];
    local p2 = r1 * [cos(ac – aw/2),sin(ac – aw/2),-];
    local p3 = r1 * [cos(ac + aw/2),sin(ac + aw/2),-];
    goto(p3);
    arc_cw(p0 +| [-,-,0],ScaleHeight/2); // blade enters surface
    move([-,-,KnifeZ]); // apply pressure
    arc_cw(p1,r0); // smallest arc
    arc_cw(p2,ScaleHeight/2); // half a circle
    arc_ccw(p3,r1);
    arc_cw(p0,ScaleHeight/2);
    arc_cw(p1 +| [-,-,TravelZ],r0); // exit from cut
    goto([0,0,-]);
    goto([-,-,SafeZ]);
    }
    //———-
    // Cut cursor outline
    CursorHubOD = 1.0in;
    CursorTipWidth = to_inch(9.0/16.0);
    CursorTipRadius = to_inch(1.0/16.0);
    function MillCursor() {
    // Mark center pivot
    MarkPivot();
    comment("Cursor outline");
    local dr = DeckBottomOD/2;
    local hr = CursorHubOD/2;
    local a = atan(hr – CursorTipWidth/2,dr); // rough & ready approximation
    local p0 = hr * [sin(a),cos(a),-]; // upper tangent point on hub
    local c1 = [dr – CursorTipRadius,CursorTipWidth/2 – CursorTipRadius*cos(a),-];
    local p1 = c1 + [CursorTipRadius*sin(a),CursorTipRadius*cos(a),-];
    local p2 = c1 + [CursorTipRadius,0,-]; // around tip radius
    feedrate(KnifeSpeed);
    goto([-,-,TravelZ]);
    goto([-hr,0,-]);
    move([-,-,EngraveZ]);
    repeat(3) {
    arc_cw(p0,hr);
    move(p1);
    arc_cw(p2,CursorTipRadius);
    move([p2.x,-p2.y,-]);
    arc_cw([p1.x,-p1.y,-],CursorTipRadius);
    move([p0.x,-p0.y,-]);
    arc_cw([-hr,0,-],hr);
    }
    goto([-,-,SafeZ]); // done, so get out of the way
    goto([0,0,-]);
    }
    //—————————————————————————–
    // The actual machining sequences!
    //—–
    // Bottom Deck
    if (SelectPart == "Bottom") {
    DeckOD = DeckBottomOD;
    DeckRad = DeckOD / 2;
    comment(" OD: ",DeckOD);
    if (Operation == "Engrave") {
    EngraveBottom();
    }
    elif (Operation == "Mill") {
    MillBottom();
    }
    else {
    error("Invalid operation: ",Operation);
    }
    }
    //——
    // Middle Deck
    if (SelectPart == "Middle") {
    DeckOD = DeckMiddleOD;
    DeckRad = DeckOD / 2;
    comment(" OD: ",DeckOD);
    if (Operation == "Engrave") {
    EngraveMiddle();
    }
    elif (Operation == "Mill") {
    MillMiddle();
    }
    else {
    error("Invalid operation: ",Operation);
    }
    }
    //—–
    // Top Deck
    if (SelectPart == "Top") {
    DeckOD = DeckTopOD;
    DeckRad = DeckOD / 2;
    comment(" OD: ",DeckOD);
    if (Operation == "Engrave") {
    EngraveTop();
    }
    elif (Operation == "Mill") {
    MillTop();
    }
    else {
    error("Invalid operation: ",Operation);
    }
    }
    //—–
    // Cursor
    if (SelectPart == "Cursor") {
    DeckOD = DeckBottomOD;
    DeckRad = DeckOD / 2;
    comment(" OD: ",DeckOD);
    if (Operation == "Engrave") {
    EngraveCursor();
    }
    elif (Operation == "Mill") {
    MillCursor();
    }
    else {
    error("Invalid operation: ",Operation);
    }
    }
    #!/bin/bash
    # Tek Circuit Computer Engraving
    # Ed Nisley KE4ZNU – 2019-11
    #OD='BaseOD=118mm' # CD = 120
    #OD='BaseOD=93mm' # hard drive = 95mm
    #EZ='EngraveZ=-5mm' # Engraving Z
    Flags='-P 3 –pedantic' # avoid leading hyphen gotcha
    # Set these to match your file layout
    ProjPath='/mnt/bulkdata/Project Files/Tektronix Circuit Computer/Firmware'
    LibPath='/opt/gcmc/library'
    Prolog='prolog.gcmc'
    Epilog='epilog.gcmc'
    ScriptPath=$ProjPath
    Script='Tek Circuit Computer.gcmc'
    #—–
    # params: deck operation
    function Runit {
    fn=TekCC-${1}-${2}.ngc
    echo "(File: "$fn")" > $fn
    sel='SelectPart="'$1'"'
    op='Operation="'$2'"'
    echo Output: $fn
    echo " "$sel
    echo " "$op
    if [ -e $fn ]
    then rm -f $fn
    fi
    gcmc -D "$OD" -D "$EZ" \
    -D "$sel" -D "$op" $Flags \
    –include "$LibPath" –prologue "$Prolog" –epilogue "$Epilog" \
    "$ScriptPath"/"$Script" >> "$fn"
    }
    #—–
    Runit Bottom Engrave
    Runit Bottom Mill
    Runit Middle Engrave
    Runit Middle Mill
    Runit Top Engrave
    Runit Top Mill
    Runit Cursor Engrave
    Runit Cursor Mill
    view raw TekCC.sh hosted with ❤ by GitHub

  • Tour Easy: Fairing Strut Mounts, Redux

    Our Young Engineer’s Tour Easy followed us home, due to a non-survivable cycling commute and inadequate apartment storage space. What with its Zzipper fairing being off and having easy access to the strut, I conjured & installed another set of fairing mounting blocks:

    Tour Easy - Fairing Strut Mount Blocks
    Tour Easy – Fairing Strut Mount Blocks

    Should you be in need of a Tour Easy recumbent in good shape, well, have I got a deal for you. I’ll even conjure a Daytime Running Light mount, if that’s what it takes …

  • CNC 3018-Pro: Collet Pen Holder

    Along the same lines as the MPCNC pen holder, I now have one for the 3018:

    CNC3018 - Collet pen holder - assembled
    CNC3018 – Collet pen holder – assembled

    The body happened to be slightly longer than two LM12UU linear bearings stacked end-to-end, which I didn’t realize must be a constraint until I was pressing them into place:

    CNC 3018-Pro Collet Holder - LM12UU - solid model
    CNC 3018-Pro Collet Holder – LM12UU – solid model

    In the unlikely event I need another one, the code will sprout a max() function in the appropriate spot.

    Drilling the aluminum rod for the knurled ring produced a really nice chip:

    CNC3018 - Collet pen holder - drilling knurled ring
    CNC3018 – Collet pen holder – drilling knurled ring

    Yeah, a good drill will produce two chips, but I’ll take what I can get.

    There’s not much left of the original holder after turning it down to 8 mm so it fits inside the 12 mm rod:

    CNC3018 - Collet pen holder - turning collet OD
    CNC3018 – Collet pen holder – turning collet OD

    Confronted by so much shiny aluminum, I realized I didn’t need an 8 mm hole through the rod, so I cut off the collet shaft and drilled out the back end to clear the flanges on the ink tubes:

    CNC3018 - Collet pen holder - drilling out collet
    CNC3018 – Collet pen holder – drilling out collet

    I figured things would eventually go badly if I trimmed enough ink-filled crimps:

    Collet holder - pen cartridge locating flanges
    Collet holder – pen cartridge locating flanges

    The OpenSCAD source code as a GitHub Gist:

    // Collet Pen Holder in LM12UU linear bearings for CNC3018
    // Ed Nisley KE4ZNU – 2019-10-30
    Layout = "Build"; // [Build, Show, Base, Mount, Plate]
    /* [Hidden] */
    ThreadThick = 0.25; // [0.20, 0.25]
    ThreadWidth = 0.40; // [0.40, 0.40]
    /* [Hidden] */
    Protrusion = 0.1; // [0.01, 0.1]
    HoleWindage = 0.2;
    inch = 25.4;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    ID = 0;
    OD = 1;
    LENGTH = 2;
    //- 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);
    }
    //- Dimensions
    PenOD = 3.5; // pen cartridge diameter
    Bearing = [12.0,21.0,30.0]; // linear bearing body
    SpringSeat = [0.56,10.0,3*ThreadThick]; // wire = ID, coil = OD, seat depth = length
    WallThick = 4.0; // minimum thickness / width
    Screw = [3.0,6.75,25.0]; // holding it all together, OD = washer
    Insert = [3.0,5.5,8.2]; // brass insert
    //Insert = [4.0,6.0,10.0];
    Clamp = [43.2,44.5,34.0]; // tool clamp ring, OD = clearance around top
    LipHeight = IntegerMultiple(2.0,ThreadThick); // above clamp for retaining
    BottomExtension = 25.0; // below clamp to reach workpiece
    MountOAL = LipHeight + Clamp[LENGTH] + BottomExtension; // total mount length
    echo(str("Mount OAL: ",MountOAL));
    Plate = [1.5*PenOD,Clamp[ID] – 0*2*WallThick,WallThick]; // spring reaction plate
    NumScrews = 3;
    ScrewBCD = Bearing[OD] + Insert[OD] + 2*WallThick;
    echo(str("Retainer max OD: ",ScrewBCD – Screw[OD]));
    NumSides = 9*4; // cylinder facets (multiple of 3 for lathe trimming)
    // Basic mount shape
    module CNC3018Base() {
    translate([0,0,MountOAL – LipHeight])
    cylinder(d=Clamp[OD],h=LipHeight,$fn=NumSides);
    translate([0,0,MountOAL – LipHeight – Clamp[LENGTH] – Protrusion])
    cylinder(d=Clamp[ID],h=(Clamp[LENGTH] + 2*Protrusion),$fn=NumSides);
    cylinder(d1=Bearing[OD] + 2*WallThick,d2=Clamp[ID],h=BottomExtension + Protrusion,$fn=NumSides);
    }
    // Mount with holes & c
    module Mount() {
    difference() {
    CNC3018Base();
    translate([0,0,-Protrusion]) // bearing
    PolyCyl(Bearing[OD],2*MountOAL,NumSides);
    for (i=[0:NumScrews – 1]) // clamp screws
    rotate(i*360/NumScrews)
    translate([ScrewBCD/2,0,MountOAL – Clamp[LENGTH]])
    rotate(180/8)
    PolyCyl(Insert[OD],Clamp[LENGTH] + Protrusion,8);
    }
    }
    module SpringPlate() {
    difference() {
    cylinder(d=Plate[OD],h=Plate[LENGTH],$fn=NumSides);
    translate([0,0,-Protrusion])
    PolyCyl(Plate[ID],2*MountOAL,NumSides);
    translate([0,0,Plate.z – SpringSeat[LENGTH]]) // spring retaining recess
    PolyCyl(SpringSeat[OD],SpringSeat[LENGTH] + Protrusion,NumSides);
    for (i=[0:NumScrews – 1]) // clamp screws
    rotate(i*360/NumScrews)
    translate([ScrewBCD/2,0,-Protrusion])
    rotate(180/8)
    PolyCyl(Screw[ID],2*MountOAL,8);
    }
    }
    //—–
    // Build it
    if (Layout == "Base")
    CNC3018Base();
    if (Layout == "Mount")
    Mount();
    if (Layout == "Plate")
    SpringPlate();
    if (Layout == "Show") {
    Mount();
    translate([0,0,1.25*MountOAL])
    rotate([180,0,0])
    SpringPlate();
    }
    if (Layout == "Build") {
    translate([0,-0.75*Clamp[OD],MountOAL])
    rotate([180,0,0])
    Mount();
    translate([0,0.75*Plate[OD],0])
    SpringPlate();
    }

  • Makergear M2: Octopi Camera Mount

    Octopirint / Octopi works wonderfully well as a controller / G-Code feeder for my Makergear M2. After putting up with an ungainly mass of tape for far too long, I printed Toddman’s Pi Camera Mount:

    Pi Camera - M2 Mount - Slic3r
    Pi Camera – M2 Mount – Slic3r

    Which snapped together exactly like it should:

    Makergear M2 - Pi Camera Mount
    Makergear M2 – Pi Camera Mount

    A strip of double-sided foam tape attaches it to the Pi’s case, which is Velcro-ed to the M2’s frame. The cable may be too long, but avoids sharp bends on the way out of the case.

    The whole lashup works fine:

    Pi Camera - M2 Mount - Octopi timelapse
    Pi Camera – M2 Mount – Octopi timelapse

    That’s a second set intended for the CNC 3018-Pro, but it didn’t fit quite as well. The B brackets are slightly too long (or their pivots are slightly too close to their base) to allow the C plates to turn 90° to the mount:

    Pi Camera - M2 Mount - Config 2 diagram
    Pi Camera – M2 Mount – Config 2 diagram

    Nothing one can’t fix with nibbling & filing, but I long for parametric designs …