-
GCMC Guilloche Plot Generator
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:

Guilloche 591991062 – scanned 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:

Guilloche 213478836 Seed = 213478837:

Guilloche 213478837 Seed = 213478838:

Guilloche 213478838 Seed = 213478839:

Guilloche 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:
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 // 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 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# 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