-
MPCNC: Spirograph Generator with Tool Changes
An improved version of my GCMC Spirograph pattern generator, now with better annotation and tool changes:

Spirograph pattern – overview The GCMC code sets the stator and rotor gear tooth counts, the rotor diameter, and the pen offset using a pseudo-random number generator. This requires randomizing the PRNG seed, which I do in the calling script with the nanosecond of the current second:
rnd=$(date +%N).The G-Code file name also comes from the timestamp:
ts=$(date +%Y%m%d-%H%M%S) fn='Spiro_'${ts}'.ngc' # blank line to make the underscore visibleWhich means you must call the Bash script slowly to generate a pile o’ plots:
for i in {1..60} ; do sh /mnt/bulkdata/Project\ Files/Mostly\ Printed\ CNC/Patterns/spiro.sh ; sleep 1 ; doneSift through the heap with drag-n-drop action using an online G-Code previewer. There seems no clean way to convert G-Code to a bitmap on the command line, although you can do it manually, of course.
The GCMC program spits out the G-code for one plot at a time, so the Bash script calls it four times to fill a sheet of paper with random patterns:
for p in $(seq 4) do rnd=$(date +%N) gcmc -D Pen=$p -D $Paper -D PRNG_Seed=$rnd $Flags $LibPath -q "$Spirograph" >> $fn done
The
-qparameter tells GCMC to not include the prolog and epilog files, because the calling script glues those onto the lump of G-Code for all four plots.The
-D Pen=$pparameter tells the GCMC program which “tool” to select with aTn M6tool change command before starting the plot. Although plotter pens have a well-defined position in the holder and a pretty nearly constant length, you must have a tool length probe installed and configured:
MPCNC Tool Length Probe – Plotter Pen Set the overall sheet size in inches or millimeters to get a plot centered in the middle of the page with half-inch margins all around:
Paper='PaperSize=[16.5in,14in]
With all that in hand, those good old black ceramic-tip pens give impeccable results:

Spirograph pattern – black ceramic pen – detail The surviving ones, anyhow. I must apply my collection of Sakura Micron pens to this task.
The other three colors come from fiber pens with reasonably good tips:

Spirograph pattern – central details They’re a lot like diatoms: all different and all alike.
The GCMC and Bash source code as a GitHub Gist:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters# Spirograph G-Code Generator # Ed Nisley KE4ZNU – December 2017 Paper='PaperSize=[16.5in,14in]' Flags="-P 2" LibPath="-I /opt/gcmc/library" Spirograph='/mnt/bulkdata/Project Files/Mostly Printed CNC/Patterns/Spirograph.gcmc' Prolog="/home/ed/.config/gcmc/prolog.gcmc" Epilog="/home/ed/.config/gcmc/epilog.gcmc" ts=$(date +%Y%m%d-%H%M%S) fn='Spiro_'${ts}'.ngc' echo Output: $fn rm -f $fn echo "(File: "$fn")" > $fn cat $Prolog >> $fn for p in $(seq 4) do rnd=$(date +%N) gcmc -D Pen=$p -D $Paper -D PRNG_Seed=$rnd $Flags $LibPath -q "$Spirograph" >> $fn done cat $Epilog >> $fn This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters// 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 elif (!(b & 1)) {b >>= 1;} // … likewise elif (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 // http://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;} elif (x < Xmin) {Xmin = x;} y = (1 – K)*sin(a) – L*K*sin(a*(1 – K)/K); if (y > Ymax) {Ymax = y;} elif (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)]; } elif (Pen == 2) { textorg = [-PlotSize.x/2,-(PlotSize.y/2 – TextLeading*TextSize.y)]; } elif (Pen == 3) { textorg = [PlotSize.x/2 – maxlength, PlotSize.y/2 – TextSize.y]; } elif (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]);