Both bCNC and GCMC include Spirograph generators with more-or-less fixed patterns and sizes, because the code serves to illustrate the software’s capabilities:

I wanted to exercise my MPCNC’s entire range of travel, familiarize myself with some new GCMC features, and, *en passant*, mimic the actual gears in a classic Spirograph, so, of course, I had to write a Spirograph emulator from scratch:

The perspective makes a 29×19 inch sheet of paper (made from three B sheets and one A sheet) look not much larger than the 17×11 inch B size sheets in the first two pictures. IRL, it’s a billboard!

My GCMC code uses notation and formulas from a paper (tidy PDF) on a Gnuplot spirograph generator, with a dash of error checking from the GCMC source.

The code enumerates the possible gear tooth counts in a pair of vectors from which you select the desired stator and rotor gears using integer subscripts. Because I eventually scale the results to fit the plot area, there’s no need to keep track of actual gear pitch diameters.

Similarly, the pen offset from the center of the rotor gear is a pure number, which you can think of as the ratio of the offset to the rotor diameter. It can have either sign and may exceed unity, as needed, either of which would be difficult with a physical gear.

Figuring the number of rotor turns required to complete the pattern requires reducing the gear ratio to a fraction with no common factors, so I wrote a Greatest Common Divisor function using Euclid’s algorithm adapted for GCMC’s bitwise tests and shifts.

With those values in hand, a loop iterates around the entire pattern to produce a list of XY coordinates in normalized space. Because the formula doesn’t have the weird properties of the Superformula I used with the HP 7475 plotter, I *think* there’s no need to prune the list to eliminate tiny moves.

Scaling the entire plot requires keeping track of the actual extents along both axes, which happens in the loop generating the normalized coordinates. A pair of gears producing an odd number of lobes can have different extents in the positive and negative directions, particularly with only a few lobes (3, 5, 7 …):

So I accumulate all four, then scale based on the absolute maximum; I added scalar `min()`

and `max()`

functions.

Converting the list of scaled points into G-Code turns out to be a one-liner using GCMC’s `tracepath()`

library function. Previewing the results in a Web-based simulator helps weed out the duds.

The code needs cleanup, in particular to let a Bash script set various parameters, but it’s a good start.

The GCMC source code as a GitHub Gist:

// Spirograph simulator for MPCNC plotter | |

// Ed Nisley KE4ZNU - 2017-12 | |

// 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 | |

include("tracepath.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; | |

// message("gcd(" + a + "," + b + ") = "); | |

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 | |

} | |

g = a*(1 << d); // form gcd | |

// message(" " + g); | |

return g; | |

} | |

//----- | |

// Max and min functions | |

function max(x,y) { | |

return (x > y) ? x : y; | |

} | |

function min(x,y) { | |

return (x < y) ? x : y; | |

} | |

//----- | |

// Spirograph tooth counts mooched from: | |

// http://nathanfriend.io/inspirograph/ | |

Stators = [96, 105, 144, 150]; | |

Rotors = [24, 30, 32, 36, 40, 45, 48, 50, 52, 56, 60, 63, 64, 72, 75, 80, 84]; | |

//----- | |

// Set up gearing | |

s = 1; // index values should be randomized | |

r = 6; | |

StatorTeeth = Stators[s]; // from the universe of possible teeth | |

RotorTeeth = Rotors[r]; | |

message("Stator: ", StatorTeeth); | |

message("Rotor: ", RotorTeeth); | |

L = 0.90; // normalized pen offset in rotor | |

message("Pen offset: ", L); | |

g = gcd(StatorTeeth,RotorTeeth); // reduce teeth to ratio of least integers | |

StatorN = StatorTeeth / g; | |

RotorM = RotorTeeth / g; | |

K = to_float(RotorM) / to_float(StatorN); // normalized rotor dia | |

Lobes = StatorN; // having removed all common factors | |

Turns = RotorM; | |

message("Lobes: ", Lobes); | |

message("Turns: ", Turns); | |

AngleStep = 2.0deg; | |

//----- | |

// 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 | |

TableSize = [25in,18in]; // largest possible plot area | |

PaperSize = 0 ? [17.0in,11.0in] : TableSize; | |

Margins = [0.5in,0.5in] * 2; | |

Boundary = PaperSize - Margins; | |

message("Boundary: ",Boundary); | |

PlotScale = [Boundary.x / (2*Xmax), Boundary.y / (2*Ymax)]; | |

message("Plot scale: ", PlotScale); | |

Points = scale(Path,PlotScale); // fill page, origin at center | |

//----- | |

// Produce G-Code | |

feedrate(6000.0mm); | |

safe_z = [-,-,25.0mm]; | |

plotz = -1.0mm; | |

goto([0,0,10.0mm]); | |

tracepath(Points, plotz); | |

goto(safe_z); |

Question. Why are you using the Protoneer/Arduino/GRBL controller instead of the Mini-RAMBo that they sell with the kit? Cheaper? Better? What you are familiar with?

Marlin on RAMPS is so tightly coupled to 3D printing that using it for anything else requires fighting its assumptions to a standstill.

GRBL implements nearly the entire LinuxCNC G-Code dialect, including good support for work coordinate systems, along with a bunch of handy M-Codes, so it’s better suited for three-axis subtractive machining. Given its intended application, the Protoneer board has just what’s needed and nothing more.

I’ve also been using bCNC on a Raspberry Pi for the UI and like it a lot. At some point, I’ll mount a Z axis probe on the tool holder and let bCNC map out the table surface, which should make it possible to do fine engraving with a hulk like the MPCNC.

GRBL seems a whole lot more mature than Marlin and, in fact, is in a steel-cage death match with program space; without room to grow, it’s entered de-facto feature freeze. I like stability in a CNC control program …

Ryan recently added de-racking homing to (feature-frozen!) Marlin, using two extruder stepper drivers as separate X and Y motor drivers. I expect the GRBL-Mega branch will eventually include de-racking, which would justify switching to a RAMBO/RAMPS board, but that won’t happen for a while.

Thanks! I knew you had reasons. I’m considering building one of these because I don’t want to invest $2k in something I might play with for a few months and then never use again. And because sometimes the journey is as much fun as the destination. :-)

In one of your posts on this topic you mentioned a drag chain being over-kill. You might be interested: recently on Hack-a-day they posted a low-cost, lightweight drag-chain solution involving woven cable-wrap and tape-measure tape. Quite clever. https://hackaday.com/2017/12/17/diy-cable-chain-looks-great-stays-cheap/

A perfect solution: thanks for the pointer!

Now, if only I hadn’t given away a box of steel tapes … [sigh]