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

3 thoughts on “Homage Tektronix Circuit Computer: Pen Plotter Version

  1. My roommate in college (pre-HP35 era) used a circular general purpose slide “rule”, er, disk. Somehow, nobody ever wanted to borrow it.

    In keeping with the climate requirements of the Midwest, I used an aluminum Pickett. Most Post(tm) sliderule owners had fun* adjusting them each season.

    (*) For various definitions of the word.

    1. The rotors on my circular Pickett were always too hard to turn. I should see if a dose of modern dry silicone lube would improve its disposition. Ya never know when you might need a slipstick, but not having used the thing in nigh onto half a century, it’s not urgent …

Comments are closed.