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 <= Turns360deg ; a += AngleStep) {
x = (1 - K)
cos(a) + LKcos(a*(1 - K)/K);
if (x > Xmax) {Xmax = x;}
else if (x < Xmin) {Xmin = x;}

y = (1 - K)sin(a) - LKsin(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 / (2Xmax), PlotSize.y / (2Ymax)];
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) {
}
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);
}

attrpath = typeset("Ed Nisley - KE4ZNU - softsolder.com",FONT_HSANS_1);
attrpath = rotate_xy(attrpath,90deg);