An improved version of my GCMC Spirograph pattern generator, now with better annotation and tool changes:

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 visible
Which 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 ; done
Sift 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 -q parameter 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=$p parameter tells the GCMC program which “tool” to select with a Tn M6 tool 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:

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:

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:

They’re a lot like diatoms: all different and all alike.
The GCMC and Bash source code as a GitHub Gist:
| # 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 |
| // 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]); |
Comments
One response to “MPCNC: Spirograph Generator with Tool Changes”
[…] GCMC Spirograph Generator program chooses parameters using pseudo-random numbers based on a seed fed in from the Bash script, so I […]