SyntaxError

// Spirograph simulator for MPCNC used as plotter
// Ed Nisley KE4ZNU - 2017-12-23
// Spirograph equations:
// https://en.wikipedia.org/wiki/Spirograph
// Loosely based on GCMC cycloids.gcmc demo:
// https://gitlab.com/gcmc/gcmc/tree/master/example/cycloids.gcmc
// Required command line parameters:
// -D Pen=n pen selection for tool change and legend position
// -D PaperSize=[x,y] overall sheet size: [17in,11in]
// -D PRNG_Seed=i non-zero random number seed
include("tracepath.inc.gcmc");
include("engrave.inc.gcmc");
//-----
// Greatest Common Divisor
// https://en.wikipedia.org/wiki/Greatest_common_divisor#Using_Euclid's_algorithm
// Inputs = integers without units
function gcd(a, b) {
local d=0;
if (!isnone(a) || isfloat(a) || !isnone(b) || isfloat(b)) {
warning("Values must be dimensionless integers");
}
while (!((a | b) & 1)) { // remove and tally common factors of two
a >>= 1;
b >>= 1;
d++;
}
while (a != b) {
if (!(a & 1)) {a >>= 1;} // discard non-common factor of 2
else if (!(b & 1)) {b >>= 1;} // ... likewise
else if (a > b) {a = (a - b) >> 1;} // gcd(a,b) also divides a-b
else {b = (b - a) >> 1;} // ... likewise
}
local GCD = a*(1 << d); // form gcd
// message("GCD: ",GCD);
return GCD;
}
//-----
// Max and min functions
function max(x,y) {
return (x > y) ? x : y;
}
function min(x,y) {
return (x < y) ? x : y;
}
//-----
// Pseudo-random number generator
// Based on xorshift:
// https://en.wikipedia.org/wiki/Xorshift
// www.jstatsoft.org/v08/i14/paper
// Requires initial state from calling script
// -D "PRNG_Seed=$(date +%N)"
function XORshift() {
local x = PRNG_State;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
PRNG_State = x;
return x;
}
//-----
// Spirograph tooth counts mooched from:
// http://nathanfriend.io/inspirograph/
// Stators has both inside and outside counts, because we're not fussy
Stators = [96, 105, 144, 150];
Rotors = [24, 30, 32, 36, 40, 45, 48, 50, 52, 56, 60, 63, 64, 72, 75, 80, 84];
//-----
// Start drawing things
// Set these variables from command line
comment("PRNG seed: ",PRNG_Seed);
PRNG_State = PRNG_Seed;
// Define some useful constants
AngleStep = 2.0deg;
Margins = [0.5in,0.5in] * 2;
comment("Paper size: ",PaperSize);
PlotSize = PaperSize - Margins;
comment("PlotSize: ",PlotSize);
//-----
// Set up gearing
s = (XORshift() & 0xffff) % count(Stators);
StatorTeeth = Stators[s];
comment("Stator ",s,": ",StatorTeeth);
r = (XORshift() & 0xffff) % count(Rotors);
RotorTeeth = Rotors[r];
comment("Rotor ",r,": ",RotorTeeth);
GCD = gcd(StatorTeeth,RotorTeeth); // reduce teeth to ratio of least integers
comment("GCD: ",GCD);
StatorN = StatorTeeth / GCD;
RotorM = RotorTeeth / GCD;
L = to_float((XORshift() & 0x3ff) - 512) / 100.0; // normalized pen offset in rotor
comment("Offset: ", L);
sgn = sign((XORshift() & 0xff) - 128);
K = sgn*to_float(RotorM) / to_float(StatorN); // normalized rotor dia
comment("Dia ratio: ",K);
Lobes = StatorN; // having removed all common factors
Turns = RotorM;
comment("Lobes: ", Lobes);
comment("Turns: ", Turns);
//-----
// Crank out a list of points in normalized coordinates
Path = {};
Xmax = 0.0;
Xmin = 0.0;
Ymax = 0.0;
Ymin = 0.0;
for (a=0.0deg ; a <= Turns*360deg ; a += AngleStep) {
x = (1 - K)*cos(a) + L*K*cos(a*(1 - K)/K);
if (x > Xmax) {Xmax = x;}
else if (x < Xmin) {Xmin = x;}
y = (1 - K)*sin(a) - L*K*sin(a*(1 - K)/K);
if (y > Ymax) {Ymax = y;}
else if (y < Ymin) {Ymin = y;}
Path += {[x,y]};
}
//message("Max X: ", Xmax, " Y: ", Ymax);
//message("Min X: ", Xmin, " Y: ", Ymin); // min will always be negative
Xmax = max(Xmax,-Xmin); // odd lobes can cause min != max
Ymax = max(Ymax,-Ymin); // ... need really truly absolute maximum
//-----
// Scale points to actual plot size
PlotScale = [PlotSize.x / (2*Xmax), PlotSize.y / (2*Ymax)];
comment("Plot scale: ", PlotScale);
Points = scale(Path,PlotScale); // fill page, origin at center
//-----
// Set up pen
if (Pen > 0) {
comment("Tool change: ",Pen);
toolchange(Pen);
}
//-----
// Plot the curve
feedrate(3000.0mm);
safe_z = 1.0mm;
plot_z = -1.0mm;
tracepath(Points, plot_z);
//-----
// Put legend in proper location
feedrate(500mm);
TextSize = [3.0mm,3.0mm];
TextLeading = 1.5; // line spacing as multiple of nominal text height
MaxPen = 4;
line1 = typeset("Seed: " + PRNG_Seed + " Stator: " + StatorTeeth + " Rotor: " + RotorTeeth,FONT_HSANS_1);
line2 = typeset("Offset: " + L + " GCD: " + GCD + " Lobes: " + Lobes + " Turns: " + Turns,FONT_HSANS_1);
maxlength = TextSize.x * max(line1[-1].x,line2[-1].x);
textpath = line1 + (line2 - [-, TextLeading, -]); // undef - n -> undef to preserve coordinates
if (Pen == 1 || Pen > MaxPen ) { // catch and fix obviously bogus pen selections
textorg = [PlotSize.x/2 - maxlength,-(PlotSize.y/2 - TextLeading*TextSize.y)];
}
else if (Pen == 2) {
textorg = [-PlotSize.x/2,-(PlotSize.y/2 - TextLeading*TextSize.y)];
}
else if (Pen == 3) {
textorg = [PlotSize.x/2 - maxlength, PlotSize.y/2 - TextSize.y];
}
else if (Pen == 4) {
textorg = [-PlotSize.x/2, PlotSize.y/2 - TextSize.y];
}
else {
Pen = 0; // squelch truly bogus pens
textorg = [0mm,0mm]; // just to define it
}
if (Pen) { // Pen = 0 suppresses legend
placepath = scale(textpath,TextSize) + textorg;
comment("Legend begins");
engrave(placepath,safe_z,plot_z);
}
if (Pen == 1) { // add attribution along right margin
attrpath = typeset("Ed Nisley - KE4ZNU - softsolder.com",FONT_HSANS_1);
attrpath = rotate_xy(attrpath,90deg);
attrorg = [PlotSize.x/2,5*TextLeading*TextSize.y - PlotSize.y/2];
placepath = scale(attrpath,TextSize) + attrorg;
comment("Attribution begins");
engrave(placepath,safe_z,plot_z);
}
goto([-,-,25.0mm]);