Skip to content

Instantly share code, notes, and snippets.

@ednisley
Created September 30, 2019 23:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ednisley/0d84401153ccb71653906181214c9584 to your computer and use it in GitHub Desktop.
Save ednisley/0d84401153ccb71653906181214c9584 to your computer and use it in GitHub Desktop.
GCMC source code: Spirograph pattern engraving for CD and HD platters
// Spirograph simulator for MPCNC used as plotter
// Ed Nisley KE4ZNU - 2017-12-23
// Adapted for Guillioche plots with ball point pens - 2018-09-25
// 2019-06 Text on circular arcs
// 2019-08 Coordinate pruning
// 2019-09 Allow L > 1.0, proper scale to fit disk
// 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
//-----
// Library routines
include("tracepath.inc.gcmc");
include("engrave.inc.gcmc");
//-----
// Define useful constants
SafeZ = 10.00mm;
TravelZ = 1.00mm;
AngleStep = 0.1deg;
Snuggly = 0.2mm; // prune coordinates when closer
TextFont = FONT_HSANS_1_RS;
TextSize = [1.5mm,1.5mm];
//-----
// Command line parameters
// -D DiskType="string"
if (!isdefined("DiskType")) {
DiskType = "CD";
}
if (DiskType != "CD" && // list all possible types
DiskType != "3.5" &&
DiskType != "TrimCD"
) {
error("Unknown disk type: ",DiskType);
}
comment("Disk type: ",DiskType); // default is "CD"
Margin = 1.5mm; // clamping margin around disk OD
DiskDia = (DiskType == "3.5") ? 95.0mm :
(DiskType == "TrimCD") ? 95.0mm :
120.0mm;
OuterDia = DiskDia - 2*Margin;
OuterRad = OuterDia / 2;
comment("Outer Diameter: ",OuterDia);
comment(" Radius: ",OuterRad);
InnerDia = (DiskType == "3.5") ? 33.0mm :
(DiskType == "TrimCD") ? 38.0mm :
38.0mm;
InnerDia = InnerDia;
InnerRad = InnerDia / 2;
comment("Inner Diameter: ",InnerDia);
comment(" Radius: ",InnerRad);
MidDia = (InnerDia + OuterDia) / 2;
MidRad = MidDia / 2;
comment("Mid Diameter: ",MidDia);
comment(" Radius: ",MidRad);
LegendDia = (DiskType == "3.5") ? 31.0mm :
(DiskType == "TrimCD") ? 31.0mm :
30.0mm;
LegendDia = LegendDia;
LegenRad = LegendDia / 2;
comment("Legend Diameter: ",LegendDia);
comment(" Radius: ",LegenRad);
// -D PRNG_Seed=integer non-zero random number seed
if (isdefined("PRNG_Seed")) { // did we get a seed?
if (!PRNG_Seed) { // .. it must not be zero
PRNG_Seed = 347221084;
}
}
else { // no incoming seed, so use a constant
PRNG_Seed = 674203941;
}
comment("PRNG seed: ",PRNG_Seed);
PRNG_State = PRNG_Seed; // set initial state
// -D various other useful tidbits
// add unit to speeds and depths: 2000mm / -3.00mm / etc
if (!isdefined("PlotSpeed")) {
PlotSpeed = 2400mm;
}
if (!isdefined("TextSpeed")) {
TextSpeed = 2000mm;
}
// Force is proportional to depth, but you must know the coefficent!
if (!isdefined("PlotZ")) {
PlotZ = (DiskType == "3.5") ? -3.00 : -0.25mm;
}
if (!isdefined("Legend")) {
Legend = "Ed Nisley - KE4ZNU - softsolder.com";
}
//-----
// Spirograph tooth counts mooched from:
// http://nathanfriend.io/inspirograph/
// Stators includes both inside and outside counts, because we're not fussy
// Stator with prime tooth count will always produce that number of lobes
// Prime numbers:
// https://en.wikipedia.org/wiki/Prime_number
// Table of primes:
// https://www.factmonster.com/math/numbers/prime-numbers-facts-examples-table-all-1000
// Must be sorted and should not exceed 127 teeth, which will make plenty of lobes
Stators = [37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127];
// Rotor tooth count chosen randomly, these are for the old method
Rotors = [24, 30, 32, 36, 40, 45, 48, 50, 52, 56, 60, 63, 64, 72, 75, 80, 84];
//Rotors = [5,7,11,13,17,19,23,31,37,41,47];
//-----
// Greatest Common Divisor
// https://en.wikipedia.org/wiki/Greatest_common_divisor#Using_Euclid's_algorithm
// Inputs = integers without units
// This is unused with prime rotor tooth counts left here for completeness
function gcd(a,b) {
if (!isnone(a) || isfloat(a) || !isnone(b) || isfloat(b)) {
error("GCD params must be dimensionless integers. a: ",a," b: ",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
// www.jstatsoft.org/v08/i14/paper
// Requires initial state from calling script
// -D "PRNG_Seed=whatever"
// Bash (et. al.) supplies nine reasonably random digits from $(date +%N)
function XORshift() {
local x = PRNG_State; // fetch current state
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
PRNG_State = x; // save state for next invocation
return x;
}
//-----
// Bend text around an arc
function ArcText(TextPath,Center,Radius,BaseAngle,Align) {
PathLength = TextPath[-1].x;
Circumf = 2*pi()*Radius;
TextAngle = to_deg(360 * PathLength / Circumf);
AlignAngle = BaseAngle + (Align == "Left" ? 0 :
Align == "Center" ? -TextAngle / 2 :
Align == "Right" ? -TextAngle :
0);
ArcPath = {};
foreach(TextPath; pt) {
if (!isundef(pt.x) && !isundef(pt.y) && isundef(pt.z)) { // XY motion, no Z
r = Radius - pt.y;
a = 360deg * (pt.x / Circumf) + AlignAngle;
ArcPath += {[r*cos(a) + Center.x, r*sin(a) + Center.y,-]};
}
elif (isundef(pt.x) && isundef(pt.y) && !isundef(pt.z)) { // no XY, Z up/down
ArcPath += {pt};
}
else {
error("Point is not pure XY or pure Z: " + to_string(pt));
}
}
return ArcPath;
}
//-----
// Set up gearing
s = (XORshift() & 0xffff) % count(Stators);
StatorTeeth = Stators[s];
comment("Stator ",s,": ",StatorTeeth);
// When Stator has prime teeth, any Rotor will have GCD = 1
if (1) {
RotorTeeth = Stators[-1];
n = 0;
while (RotorTeeth >= floor(0.95 * StatorTeeth) || RotorTeeth < 5) {
RotorTeeth = (XORshift() & 0x007f); // this is why Stator can't have more than 127 teeth
n++;
}
comment("Rotor: ",RotorTeeth," in ",n," iterations");
}
else {
r = (XORshift() & 0xffff) % count(Rotors);
RotorTeeth = Rotors[r];
comment("Rotor ",r,": ",RotorTeeth);
}
K = to_float(RotorTeeth) / to_float(StatorTeeth); // find normalized rotor dia
comment("Dia ratio K: ",K," 1/K: ",1.0/K);
GCD = gcd(StatorTeeth,RotorTeeth); // reduce teeth to ratio of least integers
comment("GCD: ",GCD);
Lobes = StatorTeeth / GCD; // compute useful values
comment("Lobes: ", Lobes);
Turns = RotorTeeth / GCD;
comment("Turns: ", Turns);
// Find normalized pen offset to never cross Stator center
if (1) {
n = 0;
do {
L = (to_float((XORshift() & 0x1f) + 1) / 32.0) * (1.0/K - 1.0); // allow L > 1.0
// comment(" test L: ",L);
n++;
} while (L >= (1.0/K - 1.0) || L < 0.01);
}
else {
n = 0;
do {
L = to_float((XORshift() & 0x1f) + 1) / 32.0; // force L < 1.0
n++;
} while (L >= (1.0/K - 1.0) || L < 0.01);
}
comment("Offset L: ", L," in ",n," iterations");
//-----
// Crank out a list of points in normalized coordinates
Path = {};
for (a = 0.0deg ; a <= Turns*360deg ; a += AngleStep) {
x = (1 - K)*cos(a) + L*K*cos(a*(1 - K)/K);
y = (1 - K)*sin(a) - L*K*sin(a*(1 - K)/K);
Path += {[x,y]};
}
//-----
// Calculate normalized limits for band traced by pen in rotor at offset L
// L was chosen to produce a band around the rotor center point
RotorMin = 1.0 - 2*K;
comment("Rotor Min: ",RotorMin);
BandCtr = 1.0 - K; // band center radius
BandMin = BandCtr - L*K; // ... min radius
BandMax = BandCtr + L*K; // ... max radius
BandAmpl = BandMax - BandCtr;
comment("Band Min: ",BandMin," Ctr: ",BandCtr," Max: ",BandMax);
//-----
// Scale normalized band to fill physical limits centered at mid-disk radius
FillPath = {};
foreach (Path; pt) {
a = atan_xy(pt); // recover angle to point
r = length(pt); // ... radius to point
br = (r - BandCtr) / BandAmpl; // remove center bias, rescale to 1.0 amplitude
dr = br * (OuterRad - MidRad); // rescale to fill disk
pr = dr + MidRad; // set at disk centerline
x = pr * cos(a); // find new XY coords
y = pr * sin(a);
FillPath += {[x,y]};
}
comment("Path has ",count(FillPath)," points");
//-----
// Prune too-snuggly physical coordinates
PointList = {FillPath[0]}; // must include first point
lp = FillPath[0];
n = 0;
foreach (FillPath; pt) {
if (length(pt - lp) <= Snuggly) { // discard too-snuggly point
n++;
}
else {
PointList += {pt}; // otherwise, add it to output
lp = pt;
}
}
PointList += {FillPath[-1]}; // ensure closure at last point
comment("Pruned ",n," points, ",count(PointList)," remaining");
//-----
// Convert coordinate list to G-Code
comment("Pattern begins");
feedrate(PlotSpeed);
goto([-,-,SafeZ]);
goto([0,0,-]);
goto([-,-,TravelZ]);
tracepath(PointList, PlotZ);
//-----
// Draw the legend
comment("Legend begins");
if (Legend) {
tp = scale(typeset(Legend,TextFont),TextSize);
tpa = ArcText(tp,[0mm,0mm],LegenRad,0deg,"Center");
feedrate(TextSpeed);
engrave(tpa,TravelZ,PlotZ);
}
tp = scale(typeset(to_string(PRNG_Seed),TextFont),TextSize);
tpa = ArcText(tp,[0mm,0mm],LegenRad,180deg,"Center");
feedrate(TextSpeed);
engrave(tpa,TravelZ,PlotZ);
goto([-,-,SafeZ]); // done, so get out of the way
goto([0,0,-]);
comment("Disk ends");
#!/bin/bash
# Guilloche and Legend Generator
# Ed Nisley KE4ZNU - 2019-06
Disk='DiskType="CD"'
PlotZ='PlotZ=-3.00mm'
Legend='Legend="Ed Nisley -- KE4ZNU -- softsolder.com"'
Flags='-P 3 --pedantic'
# Set these to match your file layout
LibPath='/opt/gcmc/library'
Prolog='/mnt/bulkdata/Project Files/CNC 3018-Pro Router/Patterns/gcmc/prolog.gcmc'
Epilog='/mnt/bulkdata/Project Files/CNC 3018-Pro Router/Patterns/gcmc/epilog.gcmc'
Script='/mnt/bulkdata/Project Files/CNC 3018-Pro Router/Patterns/Platter Engraving.gcmc'
ts=$(date +%Y%m%d-%H%M%S)
if [ -n "$1" ] # if random seed parameter exists
then
rnd=$1 # .. use it
else
rnd=$(date +%N) # .. otherwise use nanoseconds
fi
fn="Disk_${ts}_${rnd}.ngc"
echo Output: $fn
Seed="PRNG_Seed=$rnd"
rm -f $fn
echo "(File: "$fn")" > $fn
gcmc -D "$Disk" -D "$Seed" -D "$Legend" -D "$PlotZ" $Flags \
--include "$LibPath" --prologue "$Prolog" --epilogue "$Epilog" \
"$Script" >> "$fn"
@ednisley
Copy link
Author

More details on my blog at https://wp.me/poZKh-8vG

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment