GCMC Text on Arcs: Improved Version

The Tektronix Circuit Computer scale annotations read both inward (from the center) and outward (from the rim):

Text on Arcs - orientation
Text on Arcs – orientation

It’s surprisingly difficult (for me, anyhow) to see the middle FL Scale as reading upside-down, rather than mirror-image backwards.

This turned into a rewrite of the the read-outward annotation code I used for the vacuum tube reflectors. Eventually the justification and orientation options came out right:

Text-on-arcs example
Text-on-arcs example

The text baseline sits at the specified radius from the center point, regardless of its orientation, so you must offset the text path by half its height in the proper direction before handing it to the ArcText function.

The testcase shows the invocation ritual:

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

A utility function to draw scale legends stuffs some of that complexity into a bottle:

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

Which means most of the text uses a simpler invocation:

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

Arc determines the angular span of each decade, with positive values going counterclockwise. MinLog is the logarithm of the scale endpoint, so adding 1.5 puts the text angle one-and-a-half decades from MinLog and multiplying by Arc moves it in the right direction. The offset angle rotates the entire scale with respect to the 0° reference sticking out the X axis over on the right. The top picture has its 0° reference pointing north-northeast.

The GCMC source code as a GitHub Gist:

INWARD = -1; // text and tick alignment (used as integers)
OUTWARD = 1;
TEXT_LEFT = -1; // text justification
TEXT_CENTERED = 0;
TEXT_RIGHT = 1;
//-----
// 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;
}

GCMC treats variables defined inside a function as local, unless they’re already defined in an enclosing scope, whereupon you overwrite the outer variables. This wasn’t a problem in my earlier programs, but I fired the footgun with nested functions using the same local / temporary variables. Now, I ruthlessly declare truly local variables as local, except when I don’t, for what seem good reasons at the time.