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]); |
One thought on “MPCNC: Spirograph Generator with Tool Changes”
Comments are closed.