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: Slipstick

  • Drag Knife Cuttery: Entry & Exit Moves

    The first pass at cutting laminated decks for the Homage Tektronix Circuit Computer left little uncut snippets at the starting point of the cut. The point of the drag knife blade trundles along behind the cutting edge and, when the ending point equals the starting point, leaves an un-cut sliver as it’s retracted vertically:

    Drag Knife - LM12UU - knife blade detail
    Drag Knife – LM12UU – knife blade detail

    The knife blade isn’t aligned in any particular direction, so it can leave a nick on either side as it enters the deck vertically at the start of the cut.

    Gradually entering the deck along the cut line gives the blade enough time to swivel around to the proper alignment before it gets down to serious cutting. Continuing the final cut past the starting point then allows the blade to recut anything remaining from the entry move.

    The middle and top decks have windows exposing the scales:

    Tek CC - radial text example
    Tek CC – radial text example

    The paths are basically two arcs connected by semicircular cuts, but with ramps on each end recutting the entry and exit paths:

    Top Deck - Window Cut Path
    Top Deck – Window Cut Path

    The entry path in the upper left slants downward from the TravelZ level of 1.5 (-ish) mm to Z=0, with the nose of the blade holder flush against the surface and the blade sunk to its full length. The vertical path to Z=-2 (-ish) increases the cutting pressure from roughly the preload value to preload + 2*(spring rate), so the blade won’t ride up under the cutting forces.

    The path then goes completely around the window at Z=-2, then ramps up to the TravelZ level again.

    All of which produces a neat cutout that sticks to the Cricut mat when I peel the rest of the deck off:

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

    That’s a middle deck before I started laminating them, but you get the general idea.

    The GCMC code (extracted from the complete lump) looks like this:

      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]);

    Having measured the angular position of the window and its size on the original Tek CC, I compute the coordinates of the four points where the semicircular “end caps” meet the longer arcs, then connect the dots with arc_xx() functions to generate the G-Code commands. As always, using the proper radius signs requires trial & error.

    While I was at it, I added entry & exit moves for the deck’s central pivot hole and outer perimeter.

    I’m pretty sure the right CAM package would take care of that, but GCMC operates well below the CAM level.

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

  • 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
  • Homage Tektronix Circuit Computer: Ball-point Pens vs. Paper

    Extra Fine Pilot V5 pens have a 0.5 mm ball, in contrast to the 1.0 mm ball in the cheap pens I’ve been using, so they should produce much finer lines.

    Which turns out to be the case:

    Tek Circuit Computer - pen and paper comparison
    Tek Circuit Computer – pen and paper comparison

    That’s a stack of three “Homage” Tek CC bottom decks under a Genuine Tektronix Circuit Computer.

    The black scale at the top of the picture (and the bottom of the stack) came from a 1 mm cheap pen in the collet holder, the two green scales come from a 0.5 mm Pilot V5RT cartridge in its new holder, and the Original is (most likely) laser-printed back when that was a New Thing.

    As always, paper makes a big difference in the results. The brownish paper is 110 pound card stock with a relatively coarse surface finish. The white paper is ordinary 22 pound general-purpose laser / inkjet printer paper.

    The 1.0 mm pen (top) doesn’t much care what it’s writing on, producing results on the low side of OK: some light sections, no blobs. Perfectly serviceable, but not pretty.

    1.0 mm ball pen
    1.0 mm ball pen

    The Pilot V5RT really likes better paper, as it bleeds out on the card stock whenever the CNC 3018XL so much as pauses at the end of a stroke. Using white paper slows, but doesn’t completely stop, the bleeding, making the blobs survivable.

    0.5 mm ball Pilot V5RT pen
    0.5 mm ball Pilot V5RT pen

    I’ve been using card stock to get stiffer, more durable, and more easily manipulated decks, but the improved line quality on the white paper says I should laminate the decks in plastic, just like the original Tektronix design.

    No surprise there!

  • Monthly Image: CD Diffraction

    Just to see how it worked, I engraved the Tek Circuit Computer scales on scrap CDs:

    CNC 3018-Pro - front overview
    CNC 3018-Pro – front overview

    At first, I hadn’t correctly scaled the text paths, but the diffraction patterns caught my eye:

    Tek CC on CD - bottom - unscaled text
    Tek CC on CD – bottom – unscaled text

    The illumination comes from two “daylight” T8 LED tubes in a shoplight fixture, running left-to-right, so it seems I held the camera rotated 1/4 turn in landscape mode. The pix look OK either way.

    Bottom deck:

    Tek CC on CD - bottom
    Tek CC on CD – bottom

    Middle deck:

    Tek CC on CD - middle
    Tek CC on CD – middle

    Top deck, with the camera held portrait-style:

    Tek CC on CD - top
    Tek CC on CD – top

    I’m a sucker for diffraction patterns …

    The tiny engravings don’t photograph well, because they’re floating atop the transparent disc and the rainbow patterns from the data layer, but they still come out OK even when scaled to fit on a hard drive platter:

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

    Looking good!

  • Tek Circuit Computer: Drag Knife Deck Cutting

    Creating a paper version of the Tektronix Circuit Computer requires nothing more than a drag knife to cut the deck outlines:

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

    The middle deck is a disk with a notch exposing the FL scale, a cutout window exposing the inductive time constant / risetime scale, and a wee circle for the Chicago screw in the middle:

    Tek CC - middle deck outline
    Tek CC – middle deck outline

    Three angles define the notch:

      FLNotchArc = 85deg;                   // width exposing FL scale
      FLRampArc = 7deg;                     // … width of entry & exit ramps
      FLNotchOffset = 2deg;                 // … start angle from 0°

    Given those, along with the deck radius and notch height (equals the underlying scale height), calculate four points defining the start and end of the ramps and connect the dots:

      local a0 = FLNotchOffset;
      local p0 = DeckRad * [cos(a0),sin(a0),-];
    
      local a1 = a0 + FLNotchArc;
      local p1 = DeckRad * [cos(a1),sin(a1),-];
    
      goto(p0);
      move([-,-,KnifeZ]);
      arc_cw(p1,-DeckRad);          // largest arc
    
      local r = DeckRad - ScaleHeight;
      local a3 = a1 - FLRampArc;
      local p3 = r * [cos(a3),sin(a3),-];
    
      local a4 = a0 + FLRampArc;
      local p4 = r * [cos(a4),sin(a4),-];
    
      move(p3);
      arc_cw(p4,r);                 // smallest arc
    
      move(p0);                     // end of notch
    
      arc_cw([DeckRad,0,-],DeckRad);      // round off corner

    The arc_cw() functions draw arcs, as you’d expect, with a positive radius tracing the shortest arc and a negative radius for the longest arc. Although I know how that works, I must still preview the result to verify the G-Code does what I want, not what I said.

    The unhappy result of a wrong sign:

    Tek CC - middle deck outline - wrong arc sign
    Tek CC – middle deck outline – wrong arc sign

    GCMC uses the (signed) radius to generate the XY coordinates and IJ offsets for G2 commands in the preferred center format:

    G0 X88.846 Y3.103
    G1 Z-2.000
    G2 X4.653 Y88.778 I-88.846 J-3.103

    Cutting the window starts from its angular width and offset, which are hardcoded magic numbers from the Tek artifact, and proceeds similarly:

      local WindowArc = 39deg;
    
      local ac = -6 * ScaleArc;                  // 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
    
      local p0 = r0 * [cos(ac + aw/2),sin(ac + aw/2),-];
      local 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(p0);
      move([-,-,KnifeZ]);
    
      arc_cw(p1,r0);                          // smallest arc
      arc_cw(p2,ScaleHeight/2);               // half a circle
      arc_ccw(p3,r1);
      arc_cw(p0,ScaleHeight/2);

    Trust me on this: incorrect radius signs generate unrecognizable outlines. Which, of course, is why you preview the G-Code before actually cutting anything:

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

    A similar hunk of code cuts the top deck; the bottom deck is a simple circle.

    The workflow, such as it is:

    • Tape a sheet of paper (Index stock, Basis 110 = 10 mil = 0.25 mm) at the center of the 3018-ProXL platform
    • Plot (“engrave”) the scales with a pen
    • Affix paper to a Cricut sticky mat taped to the MPCNC platform
    • Touch off the origin at the middle
    • Drag-cut (“mill”) the outlines

    Less complex than it may appear, but the GCMC file now spits out two G-Code files per deck: one to engrave / draw the scales on the 3018 and another to mill / cut the outlines on the MPCNC.

  • Diamond Drag Bit: Moah Downforce!

    Engraving the Tektronix Circuit Computer bottom deck on a scrap hard drive platter suggested I’m entirely too much of a sissy about downforce on the diamond drag bit:

    Tek CC - bottom deck - HD platter - L scale
    Tek CC – bottom deck – HD platter – L scale

    That’s at Z=-5 mm for 350 g of downforce, with the spring preloaded with 100 g at a 50 g/mm rate. More or less, anyhow.

    The GCMC code automagically scales everything by the ratio of the actual platter OD to the original Tek bottom deck. Using 93 mm for a hard drive platter (actual OD = 95 mm) sets the scaling to 0.197 = 93/197, which makes the scale legends just barely visible:

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

    The thing looks lovely, though, with ticks engraved at 2400 mm/min and the text at 2000 mm/min. The problem turns out to be the time taken to run the Z axis down and up while engraving so many ticks and characters!

    I cranked on another 2 mm = 100 g of preload:

    CNC 3018-Pro - diamond bit downforce plot
    CNC 3018-Pro – diamond bit downforce plot

    The top graph shows the downforce in 0.1 mm increments, rising from 0.0 to 217 g in 0.3 mm, which illustrates what the Y intercept of the plot means in real life.

    Engraving at Z=-3 mm will now produce 350 g of downforce and cut the Z axis travel time down by a bit less than half. I have no idea what the right force might be; more experiments are in order.