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.

Author: Ed

  • Beware the Unit of Measure

    While looking for something else, I stumbled across this Amazon offer (clicky for more dots):

    Hammermill Truckload Paper
    Hammermill Truckload Paper

    Yeah, a trailer load a’ paper. Word.

    Long ago, in a universe far away, my buddy Mark One mis-read a unit of measure and ended up with a trailer load a’ Tektronix Thermal Paper. It carried a silver-based emulsion requiring constant refrigeration, so he stashed about a pallet of paper canisters under every raised floor on the IBM Poughkeepsie campus. Even though the raised floor acreage has dropped dramatically, some of it may be there to this very day.

  • Homage Tektronix Circuit Computer: Paper Matters

    To judge from the dislodged pigment grains, the original Tektronix Circuit Computer probably used then-new laser printing on good-quality paper, laminated between plastic sheets:

    Tek CC - OEM
    Tek CC – OEM

    A Pilot Precise V5RT cartridge on plain paper (20 lb 98 white), also laminated, looks pretty good:

    Tek CC - V5RT green - 20 lb plain paper
    Tek CC – V5RT green – 20 lb plain paper

    But a black V5RT pen on HP Glossy Presentation Paper (44 lb, 160 g/m²), also laminated, is spectacular:

    Tek CC - V5RT black - glossy presentation paper
    Tek CC – V5RT black – glossy presentation paper

    The glossy Presentation paper is hard enough to keep the pen ball from sinking in, producing much finer lines. In round numbers:

    • 0.2 mm – Tek laser-printed (?) original
    • 0.3 mm – green V5RT on plain paper
    • 0.2 mm – black V5RT on glossy Presentation paper

    The CNC 3018XL plotted / drew everything at 2400 mm/min = 40 mm/s, with minimal wobbulation in the lines and none worth mentioning in the characters.

    The pen ball sometimes pulls a dot of ink off the glossy paper as it rises at the end of a stroke; perhaps matte paper would produce more traction on the ink.

    You can see small blobs at the end of some strokes, but the fancy paper prevents most of the bleeding visible in the previous tests. Pilot V5 pens definitely dislike card stock.

    The results looks great in person without magnification, so maybe none of that matters.

    The pix come from the Pixel 3a camera in its microscope adapter.

  • Kenmore 158 Sewing Machine: Glare Reduction

    The additional LEDs around the needle on (one of) Mary’s Kenmore Model 158 sewing machines provide plenty of light for normal sewing, but produced too much glare on the polished steel “hand hole cover plate” (their nomenclature) for small-scale work. A matte surface seemed in order, which came from some translucent mailing labels left over from our Christmas card effort:

    Kenmore 158 - non-glare cover plate
    Kenmore 158 – non-glare cover plate

    Mailing labels probably aren’t a permanent solution, but they certainly solved the problem without delay. We’re loathe to etch the steel, as increasing the surface roughness definitely isn’t what you want, nor blacken it, for obvious reasons.

    Too much light is definitely better than too little, though.

  • Merry Christmas

    Moonrise, as seen through the pines in our yard:

    Pixel 3a Night Vision - moonrise
    Pixel 3a Night Vision – moonrise

    The Pixel 3a produces exceedingly useful low-light images, mostly by having Google’s software compensate for its tiny lens and minimal light-capture area, with the downside of turning a peaceful night scene into harsh daylight.

    Take the rest of the day off, OK?

  • 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 …