It turns out the Spirograph patterns I’d been using to wring out the MPCNC are also known as Guilloché, perhaps after the guy who invented a lathe-turning machine to engrave them. Sounds pretentious, but they still look nice:

With the ballpoint pen / knife collet holder in mind, I stripped the tool changes out of the Spirograph generator GCMC source code, set the “paper size” to a convenient 100 mm square, and tidied up the code a bit.
As with Spirograph patterns, changing the random number seed produces entirely different results. A collection differing in the last digit, previewed online:
Seed = 213478836:

Seed = 213478837:

Seed = 213478838:

Seed = 213478839:

They’re such unique snowflakes …
The Bash script now accepts a single parameter to force the PRNG seed to a value you presumably want to plot again, rather than just accept whatever the Gods of Cosmic Jest will pick for you.
The GCMC source code and Bash (or whatever) script feeding it as a GitHub Gist:
// Spirograph simulator for MPCNC used as plotter | |
// Ed Nisley KE4ZNU - 2017-12-23 | |
// Adapted for Guillioche plots with ball point pens - 2018-09-25 | |
// 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, 0 = no legend, 1 = current pen | |
// -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) { | |
if (!isnone(a) || isfloat(a) || !isnone(b) || isfloat(b)) { | |
warning("GCD params must be dimensionless integers:",a,b); | |
} | |
local d = 0; // power-of-two counter | |
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=whatever" | |
// You can get nine reasonably random digits from $(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 includes 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 initial parameters from command line! | |
comment("PRNG seed: ",PRNG_Seed); | |
PRNG_State = PRNG_Seed; | |
// Define some useful constants | |
AngleStep = 2.0deg; | |
Margins = [12mm,12mm] * 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 | |
//----- | |
// Start drawing lines | |
feedrate(1500.0mm); | |
TravelZ = 1.5mm; | |
PlotZ = -1.0mm; | |
//----- | |
// Box the outline for camera alignment | |
goto([-,-,TravelZ]); | |
goto([-PaperSize.x/2,-PaperSize.y/2,-]); | |
goto([-,-,PlotZ]); | |
foreach ( {[-1,1], [1,1], [1,-1], [-1,-1]} ; pt) { | |
move([pt.x*PaperSize.x/2,pt.y*PaperSize.y/2,-]); | |
} | |
goto([-,-,TravelZ]); | |
//----- | |
// Draw the plot | |
tracepath(Points, PlotZ); | |
//----- | |
// Add legends | |
// ... only for nonzero Pen | |
if (Pen) { | |
feedrate(250mm); | |
TextFont = FONT_HSANS_1_RS; | |
TextSize = [2.5mm,2.5mm]; | |
TextLeading = 1.5; // line spacing as multiple of nominal text height | |
line1 = typeset("Seed: " + PRNG_Seed + " Stator: " + StatorTeeth + " Rotor: " + RotorTeeth,TextFont); | |
line2 = typeset("Offset: " + L + " GCD: " + GCD + " Lobes: " + Lobes + " Turns: " + Turns,TextFont); | |
maxlength = TextSize.x * max(line1[-1].x,line2[-1].x); | |
textpath = line1 + (line2 - [-, TextLeading, -]); // undef - n -> undef to preserve coordinates | |
textorg = [-maxlength/2,PaperSize.y/2 - 0*Margins.y/2 - 2*TextLeading*TextSize.y/2]; | |
placepath = scale(textpath,TextSize) + textorg; | |
comment("Legend begins"); | |
engrave(placepath,TravelZ,PlotZ); | |
attrpath = typeset("Ed Nisley - KE4ZNU - softsolder.com",TextFont); | |
attrorg = [-(TextSize.x * attrpath[-1].x)/2,-(PaperSize.y/2 - Margins.y/2 + 2*TextLeading*TextSize.y)]; | |
placepath = scale(attrpath,TextSize) + attrorg; | |
comment("Attribution begins"); | |
engrave(placepath,TravelZ,PlotZ); | |
} | |
goto([PaperSize.x/2,PaperSize.y/2,25.0mm]); // done, so get out of the way |
# Guilloche G-Code Generator | |
# Ed Nisley KE4ZNU - September 2018 | |
Paper='PaperSize=[100mm,100mm]' | |
Flags="-P 2" | |
LibPath="-I /opt/gcmc/library" | |
Spirograph='/mnt/bulkdata/Project Files/Mostly Printed CNC/Patterns/Guilloche.gcmc' | |
Prolog="/home/ed/.config/gcmc/prolog.gcmc" | |
Epilog="/home/ed/.config/gcmc/epilog.gcmc" | |
ts=$(date +%Y%m%d-%H%M%S) | |
if [ -n "$1" ] # if parameter | |
then | |
rnd=$1 # .. use it | |
else | |
rnd=$(date +%N) # .. otherwise use nanoseconds | |
fi | |
fn='Guilloche_'${ts}_$rnd'.ngc' | |
echo Output: $fn | |
p=1 | |
rm -f $fn | |
echo "(File: "$fn")" > $fn | |
#cat $Prolog >> $fn | |
gcmc -D Pen=$p -D $Paper -D PRNG_Seed=$rnd $Flags $LibPath -G $Prolog -g $Epilog "$Spirograph" >> $fn | |
#cat $Epilog >> $fn |
2 thoughts on “GCMC Guilloche Plot Generator”
Comments are closed.