├── .gitignore ├── Animation.pde ├── CycloidDrawingMachine.pde ├── Help.pde ├── LICENSE.txt ├── Mechanics.pde ├── README.md ├── Utils.pde ├── data ├── Notch-Font.vlw ├── title.png └── title_dark.png ├── setups ├── Pattern A 150_72.txt ├── Pattern B 30_120-98.txt ├── Pattern B 30_120_98.txt ├── Pattern B 30_150_98.txt ├── Pattern B 34_120_90.txt ├── Pattern B 58_150_90.txt ├── Pattern B 58_150_90_b.txt ├── Pattern B 72_150_90.txt ├── Pattern B 74_150_90.txt ├── Pattern C 150_40_30.txt ├── Pattern C 150_40_34.txt ├── Pattern D 144_72_100.txt ├── Pattern F 120_74_100.txt ├── Pattern F 150_74_100_vb.txt ├── Pattern G 50_150_40_34.txt └── Pattern G 72_144_90_34_36.txt └── sketch.properties /.gitignore: -------------------------------------------------------------------------------- 1 | untitled.png 2 | cdm*.png 3 | override_*.tsv 4 | frame*.png 5 | frame*.svg 6 | snapshot*.png 7 | snapshot*.svg 8 | web-export/ 9 | code/ 10 | ffmpeg* 11 | *.mp4 12 | -------------------------------------------------------------------------------- /Animation.pde: -------------------------------------------------------------------------------- 1 | // Routines to support saving frames for stop motions 2 | 3 | // use T to save a snapshot of the endpoint, then position pen/rods to the beginning, and hit S to save frames. 4 | 5 | // Frames can be converted to video using 6 | 7 | // ffmpeg2 -r 30 -y -pattern_type glob -i 'frame_*.png' -vcodec libx264 -pix_fmt yuv420p -pass 1 -s 800x800 -threads 0 -f mp4 untitled.mp4 8 | import java.text.DecimalFormat; 9 | 10 | float[] tweenDest = {0,0,0,0,0,0,0,0,0,0,0,0}; 11 | float[] tweenSrce = {0,0,0,0,0,0,0,0,0,0,0,0}; 12 | float tweenSteps; 13 | float tweenIdx; 14 | boolean isTweening = false; 15 | int lastTweenSnapshot = -1; 16 | int tweenFrameCtr = 0; // this does not reset, so you can save multiple sequences from one session 17 | DecimalFormat df = new DecimalFormat("#0000"); 18 | 19 | void saveTweenSnapshot() 20 | { 21 | System.arraycopy( setupMounts[setupMode], 0, tweenDest, 0, setupMounts[setupMode].length ); 22 | tweenDest[10] = penRig.len; 23 | tweenDest[11] = penRig.angle; 24 | lastTweenSnapshot = setupMode; 25 | println("\n" + getSetupString()); 26 | println("Snapshot saved, now position pen/arms to the starting point and hit S"); 27 | } 28 | 29 | void beginTweening() 30 | { 31 | if (lastTweenSnapshot != setupMode) { 32 | println("Take a snapshot of the final state by using T, then configure connectors for the beginning state"); 33 | return; 34 | } 35 | System.arraycopy( setupMounts[setupMode], 0, tweenSrce, 0, setupMounts[setupMode].length ); 36 | tweenSrce[10] = penRig.len; 37 | tweenSrce[11] = penRig.angle; 38 | 39 | float maxTravelLength = 0; 40 | for (MountPoint mp : activeMountPoints) { 41 | if (mp.setupIdx != -1) { 42 | maxTravelLength = max(maxTravelLength, mp.getDistance(tweenDest[mp.setupIdx],tweenSrce[mp.setupIdx])); 43 | } 44 | } 45 | maxTravelLength = max(maxTravelLength, abs(tweenSrce[10]-tweenDest[10]) * abs(kPenLabelIncr)); 46 | println("Max travel length: " + maxTravelLength/inchesToPoints); 47 | tweenSteps = int(maxTravelLength); // One frame for each point of travel. 48 | println("Movie will be " + tweenSteps + " frames and will take " + tweenSteps/30.0 + " at 30 fps"); 49 | tweenIdx = 0; 50 | isTweening = true; 51 | clearPaper(); 52 | completeDrawing(); 53 | } 54 | 55 | void nextTween() 56 | { 57 | if (!isTweening) 58 | return; 59 | 60 | // save snapshot 61 | tweenFrameCtr += 1; 62 | tweenIdx += 1; 63 | 64 | saveSnapshotAs("frame_" + df.format(tweenFrameCtr) + (svgMode? ".svg" : ".png")); 65 | println("Frame: " + df.format(tweenIdx) + "/" + df.format(tweenSteps)); 66 | 67 | if (tweenIdx >= tweenSteps) { 68 | isTweening = false; 69 | println("Animation complete"); 70 | return; 71 | } 72 | 73 | for (MountPoint mp : activeMountPoints) { 74 | if (mp.setupIdx != -1) { 75 | float src = tweenSrce[mp.setupIdx]; 76 | float dst = tweenDest[mp.setupIdx]; 77 | mp.itsMountLength = src + (dst-src)*tweenIdx/(float)tweenSteps; 78 | } 79 | } 80 | float src = tweenSrce[10]; 81 | float dst = tweenDest[10]; 82 | penRig.len = src + (dst-src)*tweenIdx/(float)tweenSteps; 83 | 84 | src = tweenSrce[11]; 85 | dst = tweenDest[11]; 86 | penRig.angle = src + (dst-src)*tweenIdx/(float)tweenSteps; 87 | 88 | clearPaper(); 89 | completeDrawing(); 90 | } 91 | -------------------------------------------------------------------------------- /CycloidDrawingMachine.pde: -------------------------------------------------------------------------------- 1 | // Simulation of Cycloid Drawing Machine - latest version is on https://github.com/jbum/CycloidDrawingMachine 2 | // 3 | // Physical machine designed by Joe Freedman kickstarter.com/projects/1765367532/cycloid-drawing-machine 4 | // Processing simulation by Jim Bumgardner krazydad.com 5 | // 6 | 7 | static final float inchesToPoints = 72; // controls display scaling 8 | static final float mmToInches = 1/25.4; 9 | 10 | float seventyTwoScale = inchesToPoints / 72.0; // Don't change this 11 | 12 | int[][] setupTeeth = { 13 | {150,72}, 14 | {150,94,98,30}, 15 | {150,50,100,34,40}, 16 | {144, 100, 72}, 17 | {150, 98, 100}, 18 | {150, 100, 74}, 19 | {150,50,100,34,40,50,50}, 20 | }; 21 | 22 | float[][] setupMounts = { // mount point measurements 23 | {0, 3.3838, 10.625}, 24 | {0.82661605, 4.216946, 9.375}, 25 | {0.8973, 1.5, 12}, 26 | {4, 4, 0.8, 2, 8.625}, 27 | {0.7, 2, 4, 8, 9}, 28 | {0.7, 3.3838, 4, 0.21, 12.75, 5.5, 5.25}, 29 | {2.5, 1.0, 14.0}, 30 | }; 31 | 32 | float[][] setupPens = { 33 | {7.125,-55}, 34 | {1,-35}, 35 | {3,-90}, 36 | {5.75,-65}, 37 | {6,-90}, 38 | {7.375,-65}, 39 | {4,-90}, 40 | }; 41 | 42 | Boolean[][] setupInversions = { 43 | {true}, 44 | {true}, 45 | {false}, 46 | {false, false}, 47 | {false, false}, 48 | {false, false, false}, 49 | {false}, 50 | }; 51 | 52 | float bWidth = 18.14; 53 | float bHeight = 11.51; 54 | float pCenterX = 8.87; 55 | float pCenterY = 6.61; 56 | float toothRadius = 0.0956414*inchesToPoints; 57 | float meshGap = 1.5*mmToInches*inchesToPoints; // 1.5 mm gap needed for meshing gears 58 | PFont gFont, hFont, nFont; 59 | PImage titlePic; 60 | 61 | int setupMode = 0; // 0 = simple, 1 = moving pivot, 2 = orbiting gear, 3 = orbit gear + moving pivot 62 | 63 | ArrayList activeGears; 64 | ArrayList activeMountPoints; 65 | ArrayList rails; 66 | ArrayList activeConnectingRods; 67 | 68 | Selectable selectedObject = null; 69 | Gear crank, turnTable; 70 | MountPoint slidePoint, anchorPoint, discPoint, penMount; 71 | Channel crankRail, anchorRail, pivotRail; 72 | 73 | ConnectingRod cRod; 74 | PenRig penRig, selectPenRig = null; 75 | 76 | PGraphics paper; 77 | float paperScale = 1; 78 | float paperWidth = 9*inchesToPoints*paperScale; 79 | float crankSpeed = TWO_PI/720; // rotation per frame - 0.2 is nice. 80 | int passesPerFrame = 1; 81 | boolean hiresMode = false; 82 | boolean svgMode = false; 83 | String svgPath = ""; 84 | String svgPathFragments = ""; 85 | 86 | 87 | boolean animateMode = false; // for tweening finished drawings 88 | boolean isRecording = false; // for recording entire window 89 | boolean isStarted = false; 90 | boolean isMoving = false; 91 | boolean penRaised = true; 92 | 93 | float lastPX = -1, lastPY = -1; 94 | int myFrameCount = 0; 95 | int myLastFrame = -1; 96 | int recordCtr = 0; 97 | 98 | color[] penColors = {color(0,0,0), color(192,0,0), color(0,128,0), color(0,0,128), color(192,0,192)}; 99 | color penColor = color(0,0,0); 100 | int penColorIdx = 0; 101 | 102 | float[] penWidths = {0.5, 1, 2, 3, 5, 7}; 103 | float penWidth = 1; 104 | int penWidthIdx = 1; 105 | int loadError = 0; // 1 = gears can't snug 106 | 107 | void setup() { 108 | // 18.14*72+100 11.51*72 109 | // size(int(bWidth*inchesToPoints)+100, int(bHeight*inchesToPoints)); 110 | size(1406, 828); 111 | ellipseMode(RADIUS); 112 | gFont = createFont("EurostileBold", int(32*seventyTwoScale)); 113 | hFont = createFont("Courier", int(18*seventyTwoScale)); 114 | nFont = createFont("Helvetica-Narrow", int(9*seventyTwoScale)); // loadFont("Notch-Font.vlw"); 115 | titlePic = loadImage("title_dark.png"); 116 | 117 | gearInit(); 118 | activeGears = new ArrayList(); 119 | activeMountPoints = new ArrayList(); 120 | activeConnectingRods = new ArrayList(); 121 | 122 | rails = new ArrayList(); 123 | 124 | // Board Setup 125 | 126 | paper = createGraphics(int(paperWidth), int(paperWidth)); 127 | paper.smooth(8); 128 | clearPaper(); 129 | 130 | discPoint = new MountPoint("DP", pCenterX, pCenterY); 131 | 132 | rails.add(new LineRail(2.22, 10.21, .51, .6)); 133 | rails.add(new LineRail(3.1, 10.23, 3.1, .5)); 134 | rails.add(new LineRail(8.74, 2.41, 9.87, .47)); 135 | rails.add(new ArcRail(pCenterX, pCenterY, 6.54, radians(-68), radians(-5))); 136 | rails.add(new ArcRail(8.91, 3.91, 7.79, radians(-25), radians(15))); 137 | 138 | float[] rbegD = { 139 | 4.82, 4.96, 4.96, 4.96, 4.96, 4.96 140 | }; 141 | float[] rendD = { 142 | 7.08, 6.94, 8.46, 7.70, 7.96, 8.48 143 | }; 144 | float[] rang = { 145 | radians(-120), radians(-60), radians(-40), radians(-20), 0, radians(20) 146 | }; 147 | 148 | for (int i = 0; i < rbegD.length; ++i) { 149 | float x1 = pCenterX + cos(rang[i])*rbegD[i]; 150 | float y1 = pCenterY + sin(rang[i])*rbegD[i]; 151 | float x2 = pCenterX + cos(rang[i])*rendD[i]; 152 | float y2 = pCenterY + sin(rang[i])*rendD[i]; 153 | rails.add(new LineRail(x1, y1, x2, y2)); 154 | } 155 | 156 | drawingSetup(setupMode, true); 157 | } 158 | 159 | 160 | 161 | Gear addGear(int setupIdx, String nom) 162 | { 163 | Gear g = new Gear(setupTeeth[setupMode][setupIdx], setupIdx, nom); 164 | activeGears.add(g); 165 | return g; 166 | } 167 | 168 | MountPoint addMP(int setupIdx, String nom, Channel chan) 169 | { 170 | MountPoint mp = new MountPoint(nom, chan, setupMounts[setupMode][setupIdx], setupIdx); 171 | activeMountPoints.add(mp); 172 | return mp; 173 | } 174 | 175 | ConnectingRod addCR(int rodNbr, MountPoint slide, MountPoint anchor) 176 | { 177 | ConnectingRod cr = new ConnectingRod(slide, anchor, rodNbr); 178 | activeConnectingRods.add(cr); 179 | return cr; 180 | } 181 | 182 | PenRig addPen(MountPoint penMount) { 183 | return new PenRig(setupPens[setupMode][0], setupPens[setupMode][1], penMount); 184 | } 185 | 186 | void drawingSetup(int setupIdx, boolean resetPaper) 187 | { 188 | setupMode = setupIdx; 189 | loadError = 0; 190 | 191 | println("Drawing Setup: " + setupIdx); 192 | if (resetPaper) { 193 | isStarted = false; 194 | } 195 | penRaised = true; 196 | myFrameCount = 0; 197 | 198 | activeGears = new ArrayList(); 199 | activeMountPoints = new ArrayList(); 200 | activeConnectingRods = new ArrayList(); 201 | 202 | // Drawing Setup 203 | switch (setupIdx) { 204 | case 0: // simple set up with one gear for pen arm 205 | turnTable = addGear(0,"Turntable"); 206 | crank = addGear(1,"Crank"); 207 | crankRail = rails.get(10); 208 | pivotRail = rails.get(1); 209 | crank.mount(crankRail,0); 210 | turnTable.mount(discPoint, 0); 211 | crank.snugTo(turnTable); 212 | crank.meshTo(turnTable); 213 | 214 | slidePoint = addMP(0, "SP", pivotRail); 215 | anchorPoint = addMP(1, "AP", crank); 216 | cRod = addCR(0, slidePoint, anchorPoint); 217 | 218 | penMount = addMP(2, "EX", cRod); 219 | penRig = addPen(penMount); 220 | break; 221 | 222 | case 1: // moving fulcrum & separate crank 223 | turnTable = addGear(0,"Turntable"); 224 | crank = addGear(1,"Crank"); crank.contributesToCycle = false; 225 | Gear anchor = addGear(2,"Anchor"); 226 | Gear fulcrumGear = addGear(3,"FulcrumGear"); 227 | crankRail = rails.get(1); 228 | anchorRail = rails.get(10); 229 | pivotRail = rails.get(0); 230 | crank.mount(crankRail, 0); // will get fixed by snugto 231 | anchor.mount(anchorRail,0); 232 | fulcrumGear.mount(pivotRail, 0); // will get fixed by snugto 233 | turnTable.mount(discPoint, 0); 234 | 235 | crank.snugTo(turnTable); 236 | anchor.snugTo(turnTable); 237 | fulcrumGear.snugTo(crank); 238 | 239 | crank.meshTo(turnTable); 240 | anchor.meshTo(turnTable); 241 | fulcrumGear.meshTo(crank); 242 | 243 | slidePoint = addMP(0, "SP", fulcrumGear); 244 | anchorPoint = addMP(1, "AP", anchor); 245 | cRod = addCR(0, slidePoint, anchorPoint); 246 | penMount = addMP(2, "EX", cRod); 247 | penRig = addPen(penMount); 248 | 249 | break; 250 | 251 | case 2: // orbiting gear 252 | crankRail = rails.get(9); 253 | anchorRail = rails.get(4); 254 | pivotRail = rails.get(1); 255 | 256 | // Always need these... 257 | turnTable = addGear(0,"Turntable"); 258 | crank = addGear(1,"Crank"); crank.contributesToCycle = false; 259 | 260 | // These are optional 261 | Gear anchorTable = addGear(2,"AnchorTable"); 262 | Gear anchorHub = addGear(3,"AnchorHub"); anchorHub.contributesToCycle = false; 263 | Gear orbit = addGear(4,"Orbit"); 264 | 265 | orbit.isMoving = true; 266 | 267 | // Setup gear relationships and mount points here... 268 | crank.mount(crankRail, 0); 269 | turnTable.mount(discPoint, 0); 270 | crank.snugTo(turnTable); 271 | crank.meshTo(turnTable); 272 | 273 | anchorTable.mount(anchorRail, .315); // this is a hack - we need to allow the anchorTable to snug to the crank regardless of it's size... 274 | anchorTable.snugTo(crank); 275 | anchorTable.meshTo(crank); 276 | 277 | anchorHub.stackTo(anchorTable); 278 | anchorHub.isFixed = true; 279 | 280 | orbit.mount(anchorTable,0); 281 | orbit.snugTo(anchorHub); 282 | orbit.meshTo(anchorHub); 283 | 284 | // Setup Pen 285 | slidePoint = addMP(0, "SP", pivotRail); 286 | anchorPoint = addMP(1, "AP", orbit); 287 | cRod = addCR(0, slidePoint, anchorPoint); 288 | penMount = addMP(2, "EX", cRod); 289 | penRig = addPen(penMount); 290 | break; 291 | 292 | case 3:// 2 pen rails, variation A 293 | pivotRail = rails.get(1); 294 | Channel aRail = rails.get(10); 295 | Channel bRail = rails.get(7); 296 | turnTable = addGear(0,"Turntable"); 297 | Gear aGear = addGear(1,"A"); 298 | Gear bGear = addGear(2,"B"); 299 | 300 | turnTable.mount(discPoint, 0); 301 | aGear.mount(aRail, 0.5); 302 | aGear.snugTo(turnTable); 303 | aGear.meshTo(turnTable); 304 | 305 | bGear.mount(bRail, 0.5); 306 | bGear.snugTo(turnTable); 307 | bGear.meshTo(turnTable); 308 | 309 | slidePoint = addMP(0, "SP", aGear); 310 | anchorPoint = addMP(1, "AP", bGear); 311 | cRod = addCR(0, slidePoint, anchorPoint); 312 | 313 | MountPoint slidePoint2 = addMP(2, "SP2", pivotRail); 314 | MountPoint anchorPoint2 = addMP(3, "AP2", cRod); 315 | ConnectingRod cRod2 = addCR(1, slidePoint2, anchorPoint2); 316 | penMount = addMP(4,"EX",cRod2); 317 | penRig = addPen(penMount); 318 | 319 | break; 320 | 321 | case 4: // 2 pen rails, variation B 322 | pivotRail = rails.get(1); 323 | aRail = rails.get(10); 324 | bRail = rails.get(7); 325 | turnTable = addGear(0,"TurnTable"); 326 | aGear = addGear(1,"A"); 327 | bGear = addGear(2,"B"); 328 | 329 | turnTable.mount(discPoint, 0); 330 | aGear.mount(aRail, 0.5); 331 | aGear.snugTo(turnTable); 332 | aGear.meshTo(turnTable); 333 | 334 | bGear.mount(bRail, 0.5); 335 | bGear.snugTo(turnTable); 336 | bGear.meshTo(turnTable); 337 | 338 | slidePoint = addMP(0, "SP", pivotRail); 339 | anchorPoint = addMP(1, "AP", bGear); 340 | cRod = addCR(0, slidePoint, anchorPoint); 341 | 342 | slidePoint2 = addMP(2, "SP2", aGear); 343 | anchorPoint2 = addMP(3, "AP2", cRod); 344 | cRod2 = addCR(1, anchorPoint2, slidePoint2); 345 | 346 | penMount = addMP(4, "EX", cRod2); 347 | 348 | penRig = addPen(penMount); 349 | 350 | break; 351 | 352 | case 5: // 3 pen rails 353 | pivotRail = rails.get(1); 354 | aRail = rails.get(10); 355 | bRail = rails.get(7); 356 | turnTable = addGear(0,"Turntable"); 357 | aGear = addGear(1,"A"); 358 | bGear = addGear(2,"B"); 359 | 360 | turnTable.mount(discPoint, 0); 361 | aGear.mount(aRail, 0.5); 362 | aGear.snugTo(turnTable); 363 | aGear.meshTo(turnTable); 364 | 365 | bGear.mount(bRail, 0.5); 366 | bGear.snugTo(turnTable); 367 | bGear.meshTo(turnTable); 368 | 369 | slidePoint = addMP(0, "SP", pivotRail); 370 | anchorPoint = addMP(1, "AP", bGear); 371 | cRod = addCR(0, slidePoint, anchorPoint); 372 | 373 | slidePoint2 = addMP(2, "SP2", aGear); 374 | anchorPoint2 = addMP(3, "AP2", pivotRail); 375 | cRod2 = addCR(1, slidePoint2, anchorPoint2); 376 | 377 | MountPoint slidePoint3 = addMP(4, "SP3", cRod2); 378 | MountPoint anchorPoint3 = addMP(5, "SA3", cRod); 379 | ConnectingRod cRod3 = addCR(2, anchorPoint3, slidePoint3); 380 | penMount = addMP(6, "EX", cRod3); 381 | 382 | penRig = addPen(penMount); 383 | 384 | break; 385 | case 6: // orbiting gear with rotating fulcrum (#1 and #2 combined) 386 | crankRail = rails.get(9); 387 | anchorRail = rails.get(4); 388 | // pivotRail = rails.get(1); 389 | Channel fulcrumCrankRail = rails.get(1); 390 | Channel fulcrumGearRail = rails.get(0); 391 | 392 | // Always need these... 393 | turnTable = addGear(0,"Turntable"); 394 | crank = addGear(1,"Crank"); crank.contributesToCycle = false; 395 | 396 | // These are optional 397 | anchorTable = addGear(2,"AnchorTable"); 398 | anchorHub = addGear(3,"AnchorHub"); anchorHub.contributesToCycle = false; 399 | orbit = addGear(4,"Orbit"); 400 | 401 | Gear fulcrumCrank = addGear(5,"FulcrumCrank"); fulcrumCrank.contributesToCycle = false; 402 | fulcrumGear = addGear(6,"FulcrumOrbit"); 403 | 404 | orbit.isMoving = true; 405 | 406 | // Setup gear relationships and mount points here... 407 | crank.mount(crankRail, 0); 408 | turnTable.mount(discPoint, 0); 409 | crank.snugTo(turnTable); 410 | crank.meshTo(turnTable); 411 | 412 | anchorTable.mount(anchorRail, .315); 413 | anchorTable.snugTo(crank); 414 | anchorTable.meshTo(crank); 415 | 416 | anchorHub.stackTo(anchorTable); 417 | anchorHub.isFixed = true; 418 | 419 | orbit.mount(anchorTable,0); 420 | orbit.snugTo(anchorHub); 421 | orbit.meshTo(anchorHub); 422 | 423 | 424 | fulcrumCrank.mount(fulcrumCrankRail, 0.735+.1); 425 | fulcrumGear.mount(fulcrumGearRail, 0.29-.1); 426 | fulcrumCrank.snugTo(turnTable); 427 | fulcrumGear.snugTo(fulcrumCrank); 428 | 429 | fulcrumCrank.meshTo(turnTable); 430 | fulcrumGear.meshTo(fulcrumCrank); 431 | 432 | // Setup Pen 433 | slidePoint = addMP(0, "SP", fulcrumGear); 434 | anchorPoint = addMP(1, "AP", orbit); 435 | cRod = addCR(0, slidePoint, anchorPoint); 436 | penMount = addMP(2, "EX", cRod); 437 | penRig = addPen(penMount); 438 | 439 | break; 440 | 441 | } 442 | turnTable.showMount = false; 443 | if (loadError != 0) { 444 | println("Load Error" + loadError); 445 | } 446 | } 447 | 448 | void draw() 449 | { 450 | 451 | // Crank the machine a few times, based on current passesPerFrame - this generates new gear positions and drawing output 452 | for (int p = 0; p < passesPerFrame; ++p) { 453 | if (isMoving) { 454 | myFrameCount += 1; 455 | turnTable.crank(myFrameCount*crankSpeed); // The turntable is always the root of the propulsion chain, since it is the only required gear. 456 | 457 | // work out coords on unrotated paper 458 | PVector nib = penRig.getPosition(); 459 | float dx = nib.x - pCenterX*inchesToPoints; 460 | float dy = nib.y - pCenterY*inchesToPoints; 461 | float a = atan2(dy, dx); 462 | float l = sqrt(dx*dx + dy*dy); 463 | float px = paperWidth/2 + cos(a-turnTable.rotation)*l*paperScale; 464 | float py = paperWidth/2 + sin(a-turnTable.rotation)*l*paperScale; 465 | 466 | // paper.smooth(8); 467 | paper.beginDraw(); 468 | if (!isStarted) { 469 | svgPath = ""; 470 | svgPathFragments = ""; 471 | paper.clear(); 472 | paper.noFill(); 473 | paper.stroke(penColor); 474 | paper.strokeJoin(ROUND); 475 | paper.strokeCap(ROUND); 476 | paper.strokeWeight(penWidth); 477 | // paper.rect(10, 10, paperWidth-20, paperWidth-20); 478 | isStarted = true; 479 | } else if (!penRaised) { 480 | paper.line(lastPX, lastPY, px, py); 481 | if (svgPath.isEmpty()) { 482 | svgPath = String.format("M%.1f,%.1f",lastPX,lastPY); 483 | } 484 | svgPath += String.format("L%.1f,%.1f",px,py); 485 | } 486 | paper.endDraw(); 487 | lastPX = px; 488 | lastPY = py; 489 | penRaised = false; 490 | if (myLastFrame != -1 && myFrameCount >= myLastFrame) { 491 | myLastFrame = -1; 492 | passesPerFrame = 1; 493 | isMoving = false; 494 | isRecording = false; 495 | svgPath += "Z"; 496 | nextTween(); 497 | break; 498 | } 499 | } 500 | } 501 | 502 | // Draw the machine onscreen in it's current state 503 | background(128); 504 | pushMatrix(); 505 | drawFulcrumLabels(); 506 | 507 | fill(200); 508 | noStroke(); 509 | 510 | float logoScale = inchesToPoints/72.0; 511 | image(titlePic, 0, height-titlePic.height*logoScale, titlePic.width*logoScale, titlePic.height*logoScale); 512 | for (Channel ch : rails) { 513 | ch.draw(); 514 | } 515 | 516 | for (Gear g : activeGears) { 517 | if (g != turnTable) 518 | g.draw(); 519 | } 520 | turnTable.draw(); // draw this last 521 | 522 | penRig.draw(); 523 | 524 | pushMatrix(); 525 | translate(pCenterX*inchesToPoints, pCenterY*inchesToPoints); 526 | rotate(turnTable.rotation); 527 | image(paper, -paperWidth/(2*paperScale), -paperWidth/(2*paperScale), paperWidth/paperScale, paperWidth/paperScale); 528 | popMatrix(); 529 | 530 | helpDraw(); // draw help if needed 531 | 532 | popMatrix(); 533 | if (isMoving && isRecording) { 534 | recordCtr++; 535 | saveSnapshotAs("record_" + df.format(recordCtr) + ".png"); 536 | } 537 | } 538 | 539 | boolean isShifting = false; 540 | 541 | void keyReleased() { 542 | if (key == CODED) { 543 | if (keyCode == SHIFT) 544 | isShifting = false; 545 | } 546 | } 547 | 548 | void keyPressed() { 549 | switch (key) { 550 | case ' ': 551 | isMoving = !isMoving; 552 | myLastFrame = -1; 553 | println("Current cycle length: " + myFrameCount / (TWO_PI/crankSpeed)); 554 | 555 | break; 556 | case '?': 557 | toggleHelp(); 558 | break; 559 | case '0': 560 | isMoving = false; 561 | passesPerFrame = 0; 562 | myLastFrame = -1; 563 | println("Current cycle length: " + myFrameCount / (TWO_PI/crankSpeed)); 564 | break; 565 | case '1': 566 | passesPerFrame = 1; 567 | isMoving = true; 568 | break; 569 | case '2': 570 | case '3': 571 | case '4': 572 | case '5': 573 | case '6': 574 | case '7': 575 | case '8': 576 | case '9': 577 | passesPerFrame = int(map((key-'0'),2,9,10,360)); 578 | isMoving = true; 579 | break; 580 | case 'a': 581 | case 'b': 582 | case 'c': 583 | case 'd': 584 | case 'e': 585 | case 'f': 586 | case 'g': 587 | deselect(); 588 | drawingSetup(key - 'a', false); 589 | break; 590 | case 'X': 591 | case 'x': 592 | clearPaper(); 593 | break; 594 | case 'p': 595 | // Swap pen mounts - need visual feedback 596 | break; 597 | case 's': 598 | saveSnapshot("snapshot_"); 599 | break; 600 | case 19: 601 | saveSettings(); 602 | break; 603 | case 15: 604 | loadSettings(); 605 | break; 606 | case '~': 607 | case '`': 608 | completeDrawing(); 609 | break; 610 | case 'M': 611 | measureGears(); 612 | break; 613 | case 'T': 614 | saveTweenSnapshot(); 615 | break; 616 | case 'S': 617 | beginTweening(); 618 | break; 619 | case 'R': 620 | case 'r': 621 | isRecording = !isRecording; 622 | println("Recording is " + (isRecording? "ON" : "OFF")); 623 | break; 624 | case 'H': 625 | toggleHiresmode(); 626 | break; 627 | case 'V': 628 | toggleOutputmode(); 629 | break; 630 | case '[': 631 | advancePenColor(-1); 632 | break; 633 | case ']': 634 | advancePenColor(1); 635 | break; 636 | case '<': 637 | advancePenWidth(-1); 638 | break; 639 | case '>': 640 | advancePenWidth(1); 641 | break; 642 | case '/': 643 | invertConnectingRod(); 644 | break; 645 | case '+': 646 | case '-': 647 | case '=': 648 | int direction = (key == '+' || key == '='? 1 : -1); 649 | nudge(direction, keyCode); 650 | break; 651 | case CODED: 652 | switch (keyCode) { 653 | case UP: 654 | case DOWN: 655 | case LEFT: 656 | case RIGHT: 657 | direction = (keyCode == RIGHT || keyCode == UP? 1 : -1); 658 | nudge(direction, keyCode); 659 | break; 660 | case SHIFT: 661 | isShifting = true; 662 | break; 663 | default: 664 | // println("KeyCode pressed: " + (0 + keyCode)); 665 | break; 666 | } 667 | break; 668 | default: 669 | // println("Key pressed: " + (0 + key)); 670 | break; 671 | } 672 | } 673 | 674 | void mousePressed() 675 | { 676 | deselect(); 677 | 678 | for (MountPoint mp : activeMountPoints) { 679 | if (mp.isClicked(mouseX, mouseY)) { 680 | mp.select(); 681 | selectedObject= mp; 682 | return; 683 | } 684 | } 685 | 686 | if (penRig.isClicked(mouseX, mouseY)) { 687 | penRig.select(); 688 | selectedObject= penRig; 689 | return; 690 | } 691 | 692 | for (ConnectingRod cr : activeConnectingRods) { 693 | if (cr.isClicked(mouseX, mouseY)) { 694 | cr.select(); 695 | selectedObject= cr; 696 | return; 697 | } 698 | } 699 | 700 | for (Gear g : activeGears) { 701 | if (g.isClicked(mouseX, mouseY)) { 702 | deselect(); 703 | g.select(); 704 | selectedObject = g; 705 | } 706 | } 707 | } 708 | -------------------------------------------------------------------------------- /Help.pde: -------------------------------------------------------------------------------- 1 | boolean startupAlert = true; 2 | boolean drawHelp = false; 3 | long helpStartMS = millis(); 4 | String[] helpLines = { 5 | "0-9 set drawing speed", 6 | "a-g change setups", 7 | "arrows change gears and mount points", 8 | "x erase the paper", 9 | "[ ] change pen color", 10 | "< > change pen width", 11 | "/ invert connecting rod", 12 | "s save the image", 13 | "~ draw entire cycle", 14 | "R record each frame to a png file", 15 | "R~ record entire cycle to pngs", 16 | "T save tween endpoint", 17 | "S animate finished drawings to endpoint", 18 | "H toggle Hi-res", 19 | "V toggle SVG/PNG output", 20 | "Ctrl-S save setup", 21 | "Ctrl-O load setup", 22 | }; 23 | 24 | void helpDraw() 25 | { 26 | if (drawHelp) { 27 | long elapsed = millis() - helpStartMS; 28 | float alpha = constrain(map(elapsed, 10*1000, 13*1000, 1, 0),0,1); 29 | if (alpha <= 0.0001) { 30 | drawHelp = false; 31 | } 32 | noStroke(); 33 | 34 | float hx = width-500*seventyTwoScale; 35 | float hy = 30*seventyTwoScale-100*constrain(map(elapsed,0,300,1,0),0,1); 36 | 37 | fill(255,alpha*alpha*192); 38 | rect(hx-8, 0, width-(hx-8), hy + 22*helpLines.length); 39 | 40 | fill(100, alpha*alpha*255); 41 | 42 | textFont(hFont); 43 | textAlign(LEFT); 44 | for (int i = 0; i < helpLines.length; ++i) { 45 | text(helpLines[i], hx, hy+22*i); 46 | } 47 | } 48 | else if (startupAlert) { 49 | long elapsed = millis() - helpStartMS; 50 | float alpha = constrain(map(elapsed, 10*1000, 13*1000, 1, 0),0,1); 51 | if (alpha <= 0.0001) { 52 | startupAlert = false; 53 | } 54 | textFont(hFont); 55 | textAlign(LEFT); 56 | fill(200, alpha*alpha*255); 57 | text("Press SPACE to draw, ? for Help", width-400*seventyTwoScale, 30*seventyTwoScale); 58 | } 59 | } 60 | 61 | void toggleHelp() 62 | { 63 | if (drawHelp) { 64 | drawHelp = false; 65 | } else { 66 | drawHelp = true; 67 | startupAlert = false; 68 | helpStartMS = millis(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Gabriele Cirulli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Mechanics.pde: -------------------------------------------------------------------------------- 1 | // import java.awt.Toolkit; 2 | 3 | interface Channel { 4 | PVector getPosition(float r); 5 | void draw(); 6 | void snugTo(Gear moveable, Gear fixed); // position moveable gear on this channel so it is snug to fixed gear, not needed for all channels 7 | } 8 | 9 | interface Selectable { 10 | void select(); 11 | void unselect(); 12 | void nudge(int direction, int keycode); 13 | } 14 | 15 | static final float kMPDefaultRadius = inchesToPoints * 12/72.0; 16 | static final float kMPSlideRadius = inchesToPoints * 20/72.0; 17 | static final float kGearMountRadius = inchesToPoints * 12/72.0; 18 | // static final float kGearNotchWidth = inchesToPoints * 16.5/72.0; 19 | static final float kGearNotchWidth = 5 * mmToInches * inchesToPoints; 20 | static final float kGearNotchHeightMaj = 5 * mmToInches * inchesToPoints; 21 | static final float kGearNotchHeightMin = 3.5 * mmToInches * inchesToPoints; 22 | static final float kGearLabelStart = 0.5*inchesToPoints; 23 | static final float kGearLabelIncr = 0.5*inchesToPoints; 24 | static final float kCRLabelIncr = 0.5*inchesToPoints; 25 | static final float kCRNotchIncr = 0.25*inchesToPoints; 26 | static final float kCRNotchStart = 0.75*inchesToPoints; 27 | static final float kCRLabelStart = 1*inchesToPoints; 28 | static final float kPenLabelStart = 4.75*inchesToPoints; 29 | static final float kPenLabelIncr = -0.5*inchesToPoints; 30 | static final float kPenNotchIncr = -0.25*inchesToPoints; 31 | static final float kPaperRad = 4.625*inchesToPoints; 32 | class MountPoint implements Channel, Selectable { 33 | Channel itsChannel = null; 34 | float itsMountLength; 35 | int setupIdx; 36 | float x, y, radius=kMPDefaultRadius; 37 | String typeStr = "MP"; 38 | boolean isFixed = false; 39 | boolean selected = false; 40 | 41 | MountPoint(String typeStr, float x, float y) { 42 | this.typeStr = typeStr; 43 | this.itsChannel = null; 44 | this.itsMountLength = 0; 45 | this.isFixed = true; 46 | this.setupIdx = -1; // fixed 47 | this.x = x*inchesToPoints; 48 | this.y = y*inchesToPoints; 49 | // println(this.typeStr + " is at " + this.x/inchesToPoints + "," + this.y/inchesToPoints); 50 | } 51 | 52 | MountPoint(String typeStr, Channel ch, float mr, int setupIdx) { 53 | this.typeStr = typeStr; 54 | this.itsChannel = ch; 55 | this.itsMountLength = mr; 56 | this.setupIdx = setupIdx; 57 | PVector pt = ch.getPosition(mr); 58 | this.x = pt.x; 59 | this.y = pt.y; 60 | // println(this.typeStr + " is at " + this.x/inchesToPoints + "," + this.y/inchesToPoints); 61 | } 62 | 63 | int chooseBestDirection(int direction, int keycode, float incr) 64 | { 65 | PVector pNeg = itsChannel.getPosition(itsMountLength-incr); 66 | PVector pPos = itsChannel.getPosition(itsMountLength+incr); 67 | switch (keycode) { 68 | case RIGHT: 69 | return (pPos.x >= pNeg.x)? 1 : -1; 70 | case LEFT: 71 | return (pPos.x <= pNeg.x)? 1 : -1; 72 | case UP: 73 | return (pPos.y <= pNeg.y)? 1 : -1; 74 | case DOWN: 75 | return (pPos.y >= pNeg.y)? 1 : -1; 76 | default: 77 | return direction; 78 | } 79 | } 80 | 81 | void nudge(int direction, int keycode) { 82 | float amt, mn=0, mx=1; 83 | if (itsChannel instanceof ConnectingRod) { 84 | amt = 0.125; 85 | mn = 0.5; 86 | mx = 29; 87 | } else if (itsChannel instanceof Gear) { 88 | amt = 0.125; 89 | mn = 0.75; 90 | mx = ((Gear) itsChannel).radius/(kGearLabelIncr) - 1; 91 | } else { 92 | amt = 0.01; 93 | } 94 | direction = chooseBestDirection(direction, keycode, amt); 95 | amt *= direction; 96 | itsMountLength += amt; 97 | itsMountLength = constrain(itsMountLength, mn, mx); 98 | println("Mount: " + itsMountLength); 99 | if (setupIdx >= 0) { 100 | setupMounts[setupMode][setupIdx] = itsMountLength; 101 | } 102 | } 103 | 104 | float getDistance(float v1, float v2) { 105 | PVector src = itsChannel.getPosition(v1); 106 | PVector dst = itsChannel.getPosition(v2); 107 | return dist(src.x, src.y, dst.x, dst.y); 108 | } 109 | 110 | void unselect() { 111 | selected = false; 112 | } 113 | 114 | void select() { 115 | selected = true; 116 | } 117 | 118 | boolean isClicked(int mx, int my) { 119 | PVector p = this.getPosition(); 120 | return dist(mx, my, p.x, p.y) <= this.radius; 121 | } 122 | 123 | PVector getPosition() { 124 | return getPosition(0.0); 125 | } 126 | 127 | PVector getPosition(float r) { 128 | if (itsChannel != null) { 129 | return itsChannel.getPosition(itsMountLength); 130 | } else { 131 | return new PVector(x,y); 132 | } 133 | } 134 | 135 | void snugTo(Gear moveable, Gear fixed) { // not meaningful 136 | } 137 | 138 | void draw() { 139 | PVector p = getPosition(); 140 | if (itsChannel instanceof ConnectingRod) { 141 | itsChannel.draw(); 142 | } 143 | if (selected) { 144 | fill(180,192); 145 | stroke(50); 146 | } else { 147 | fill(180,192); 148 | stroke(100); 149 | } 150 | strokeWeight(selected? 4 : 2); 151 | ellipse(p.x, p.y, this.radius, this.radius); 152 | } 153 | } 154 | 155 | class ConnectingRod implements Channel, Selectable { 156 | MountPoint itsSlide = null; 157 | MountPoint itsAnchor = null; 158 | float armAngle = 0; 159 | int rodNbr = 0; 160 | boolean selected=false; 161 | boolean isInverted = false; 162 | 163 | ConnectingRod(MountPoint itsSlide, MountPoint itsAnchor, int rodNbr) 164 | { 165 | this.rodNbr = rodNbr; 166 | this.itsSlide = itsSlide; 167 | itsSlide.radius = kMPSlideRadius; 168 | this.itsAnchor = itsAnchor; 169 | 170 | if (setupInversions[setupMode][rodNbr]) 171 | invert(); 172 | 173 | } 174 | 175 | PVector getPosition(float r) { 176 | PVector ap = itsAnchor.getPosition(); 177 | PVector sp = itsSlide.getPosition(); 178 | armAngle = atan2(sp.y - ap.y, sp.x - ap.x); 179 | float d = notchToDist(r); 180 | return new PVector(ap.x + cos(armAngle)*d, ap.y + sin(armAngle)*d); 181 | } 182 | 183 | void snugTo(Gear moveable, Gear fixed) { 184 | // not relevant for connecting rods 185 | } 186 | 187 | void unselect() { 188 | selected = false; 189 | } 190 | 191 | void select() { 192 | selected = true; 193 | } 194 | 195 | void invert() { 196 | this.isInverted = !this.isInverted; 197 | setupInversions[setupMode][rodNbr] = this.isInverted; 198 | 199 | MountPoint tmp = itsAnchor; 200 | itsAnchor = itsSlide; 201 | itsSlide = tmp; 202 | itsAnchor.radius = kMPDefaultRadius; 203 | itsSlide.radius = kMPSlideRadius; 204 | if (penRig != null && penRig.itsRod == this) { 205 | penRig.angle += 180; 206 | if (penRig.angle > 360) 207 | penRig.angle -= 360; 208 | setupPens[setupMode][1] = penRig.angle; 209 | } 210 | println("Inverted rod " + rodNbr); 211 | } 212 | 213 | void nudge(int direction, int kc) { 214 | if (kc == UP || kc == DOWN) { 215 | this.invert(); 216 | } 217 | else { 218 | if (penRig.itsRod == this) { 219 | penRig.itsMP.nudge(direction, kc); 220 | } 221 | } 222 | } 223 | 224 | boolean isClicked(int mx, int my) 225 | { 226 | PVector ap = itsAnchor.getPosition(); 227 | PVector sp = itsSlide.getPosition(); 228 | 229 | // mx,my, ap, ep522 293 546.1399 168.98767 492.651 451.97696 230 | int gr = 5; 231 | return (mx > min(ap.x-gr,sp.x-gr) && mx < max(ap.x+gr,sp.x+gr) && 232 | my > min(ap.y-gr,sp.y-gr) && my < max(ap.y+gr,sp.y+gr) && 233 | abs(atan2(my-sp.y,mx-sp.x) - atan2(ap.y-sp.y,ap.x-sp.x)) < radians(10)); 234 | } 235 | 236 | float notchToDist(float n) { 237 | return kCRLabelStart+(n-1)*kCRLabelIncr; 238 | } 239 | 240 | float distToNotch(float d) { 241 | return 1 + (d - kCRLabelStart)/kCRLabelIncr; 242 | } 243 | 244 | 245 | void draw() { 246 | PVector ap = itsAnchor.getPosition(); 247 | PVector sp = itsSlide.getPosition(); 248 | 249 | 250 | itsSlide.draw(); 251 | itsAnchor.draw(); 252 | 253 | noFill(); 254 | int shade = selected? 100 : 200; 255 | int alfa = selected? 192 : 192; 256 | stroke(shade, alfa); 257 | strokeWeight(.33*inchesToPoints); 258 | armAngle = atan2(sp.y - ap.y, sp.x - ap.x); 259 | float L = 18 * inchesToPoints; 260 | line(ap.x,ap.y, ap.x+cos(armAngle)*L, ap.y+sin(armAngle)*L); 261 | 262 | stroke(100,100,100,128); 263 | fill(100,100,100); 264 | strokeWeight(0.5); 265 | // float notchOffset = 0.75*inchesToPoints; 266 | textFont(nFont); 267 | textAlign(CENTER); 268 | pushMatrix(); 269 | translate(sp.x,sp.y); 270 | rotate(atan2(ap.y-sp.y,ap.x-sp.x)); 271 | float ln = dist(ap.x,ap.y,sp.x,sp.y); 272 | for (int i = 0; i < 29*2; ++i) { 273 | float x = ln-(kCRNotchStart + kCRNotchIncr*i); 274 | line(x, 6, x, -(6+(i % 2 == 1? 2 : 0))); 275 | if (i % 2 == 1) { 276 | text(""+(1+i/2),x,8); 277 | } 278 | } 279 | popMatrix(); 280 | } 281 | } 282 | 283 | class PenRig implements Selectable { 284 | float len; 285 | float angle; // expressed in degrees 286 | boolean selected = false; 287 | ConnectingRod itsRod; 288 | MountPoint itsMP; 289 | int lastDirection = -1; // these are used to avoid rotational wierdness with manipulations 290 | long lastRotation = -1; 291 | int lastKey = -1; 292 | 293 | 294 | PenRig(float len, float angle, MountPoint itsMP) { 295 | this.len = len; // in pen notch units 296 | this.angle = angle; 297 | this.itsRod = (ConnectingRod) itsMP.itsChannel; 298 | this.itsMP = itsMP; 299 | 300 | PVector ap = itsMP.getPosition(); 301 | PVector ep = this.getPosition(); 302 | } 303 | 304 | float notchToDist(float n) { 305 | return kPenLabelStart+(n-1)*kPenLabelIncr; 306 | } 307 | 308 | float distToNotch(float d) { 309 | return 1 + (d - kPenLabelStart)/kPenLabelIncr; 310 | } 311 | 312 | PVector getPosition() { 313 | PVector ap = itsMP.getPosition(); 314 | float d = notchToDist(this.len); 315 | float rangle = radians(this.angle); 316 | return new PVector(ap.x + cos(itsRod.armAngle + rangle)*d, ap.y + sin(itsRod.armAngle + rangle)*d); 317 | } 318 | 319 | PVector getPosition(float len, float angle) { 320 | PVector ap = itsMP.getPosition(); 321 | float d = notchToDist(len); 322 | float rangle = radians(angle); 323 | return new PVector(ap.x + cos(itsRod.armAngle + rangle)*d, ap.y + sin(itsRod.armAngle + rangle)*d); 324 | } 325 | 326 | 327 | boolean isClicked(int mx, int my) 328 | { 329 | PVector ap = itsMP.getPosition(); 330 | PVector ep = this.getPosition(); 331 | 332 | float a = atan2(ap.y-ep.y,ap.x-ep.x); 333 | float d = 6*inchesToPoints; 334 | ap.x = ep.x + cos(a)*d; 335 | ap.y = ep.y + sin(a)*d; 336 | 337 | int gr = 5; 338 | return (mx > min(ap.x-gr,ep.x-gr) && mx < max(ap.x+gr,ep.x+gr) && 339 | my > min(ap.y-gr,ep.y-gr) && my < max(ap.y+gr,ep.y+gr) && 340 | abs(atan2(my-ep.y,mx-ep.x) - atan2(ap.y-ep.y,ap.x-ep.x)) < radians(10)); 341 | } 342 | 343 | void unselect() { 344 | selected = false; 345 | } 346 | 347 | void select() { 348 | selected = true; 349 | } 350 | 351 | 352 | int chooseBestDirection(int direction, int keycode, float lenIncr, float angIncr) 353 | { 354 | if (abs(angIncr) > abs(lenIncr) && lastRotation != -1 && (millis()-lastRotation) < 10000) { 355 | return lastDirection * (lastKey == keycode? 1 : -1); 356 | } 357 | 358 | PVector pNeg = getPosition(len -lenIncr, angle-angIncr); 359 | PVector pPos = getPosition(len +lenIncr, angle+angIncr); 360 | 361 | switch (keycode) { 362 | case RIGHT: 363 | return (pPos.x >= pNeg.x)? 1 : -1; 364 | case LEFT: 365 | return (pPos.x <= pNeg.x)? 1 : -1; 366 | case UP: 367 | return (pPos.y <= pNeg.y)? 1 : -1; 368 | case DOWN: 369 | return (pPos.y >= pNeg.y)? 1 : -1; 370 | default: 371 | return direction; 372 | } 373 | } 374 | 375 | void nudge(int direction, int kc) { 376 | float angIncr = 0, lenIncr = 0; 377 | if (kc == RIGHT || kc == LEFT) { 378 | angIncr = 5; 379 | } else { 380 | lenIncr = 0.125; 381 | } 382 | direction = chooseBestDirection(direction, kc, lenIncr, angIncr); 383 | 384 | if (abs(angIncr) > abs(lenIncr)) { 385 | lastRotation = millis(); 386 | } 387 | lastDirection = direction; 388 | lastKey = kc; 389 | 390 | this.angle += angIncr*direction; 391 | if (this.angle > 180) { 392 | this.angle -= 360; 393 | } else if (this.angle <= -180) { 394 | this.angle += 360; 395 | } 396 | setupPens[setupMode][1] = this.angle; 397 | this.len += lenIncr*direction; 398 | this.len = constrain(this.len, 1, 8); 399 | setupPens[setupMode][0] = this.len; 400 | 401 | println("Pen: " + this.len + " " + this.angle + "°"); 402 | } 403 | 404 | void draw() { 405 | itsMP.draw(); 406 | PVector ap = itsMP.getPosition(); 407 | PVector ep = this.getPosition(); 408 | 409 | float a = atan2(ap.y-ep.y,ap.x-ep.x); 410 | float d = 6*inchesToPoints; 411 | ap.x = ep.x + cos(a)*d; 412 | ap.y = ep.y + sin(a)*d; 413 | 414 | noFill(); 415 | if (selected) 416 | stroke(penColor,128); 417 | else 418 | stroke(penColor,64); 419 | strokeWeight(.33*inchesToPoints); 420 | line(ap.x, ap.y, ep.x, ep.y); 421 | 422 | float nibRad = inchesToPoints * 0.111; 423 | 424 | strokeWeight(0.5); 425 | pushMatrix(); 426 | translate(ep.x,ep.y); 427 | rotate(atan2(ap.y-ep.y,ap.x-ep.x)); 428 | fill(255); 429 | ellipse(0,0,nibRad,nibRad); 430 | noFill(); 431 | stroke(192); 432 | line(-nibRad,0,nibRad,0); 433 | line(0,nibRad,0,-nibRad); 434 | 435 | textFont(nFont); 436 | textAlign(CENTER); 437 | fill(penColor); 438 | noStroke(); 439 | ellipse(0,0,penWidth/2, penWidth/2); 440 | 441 | stroke(255); 442 | fill(255); 443 | for (int i = 0; i < 15; ++i) { 444 | float x = notchToDist(1+i/2.0); 445 | line(x, 6, x, -(6+(i % 2 == 0? 2 : 0))); 446 | if (i % 2 == 0) { 447 | text(""+(1+i/2),x,8); 448 | } 449 | } 450 | popMatrix(); 451 | 452 | 453 | } 454 | } 455 | 456 | class LineRail implements Channel { 457 | float x1,y1, x2,y2; 458 | LineRail(float x1, float y1, float x2, float y2) { 459 | this.x1 = x1*inchesToPoints; 460 | this.y1 = y1*inchesToPoints; 461 | this.x2 = x2*inchesToPoints; 462 | this.y2 = y2*inchesToPoints; 463 | } 464 | PVector getPosition(float r) { 465 | return new PVector(x1+(x2-x1)*r, y1+(y2-y1)*r); 466 | } 467 | 468 | void draw() { 469 | noFill(); 470 | stroke(110); 471 | strokeWeight(.23*inchesToPoints); 472 | 473 | line(x1,y1, x2,y2); 474 | } 475 | 476 | void snugTo(Gear moveable, Gear fixed) { 477 | float dx1 = x1-fixed.x; 478 | float dy1 = y1-fixed.y; 479 | float dx2 = x2-fixed.x; 480 | float dy2 = y2-fixed.y; 481 | float a1 = atan2(dy1,dx1); 482 | float a2 = atan2(dy2,dx2); 483 | float d1 = dist(x1,y1,fixed.x,fixed.y); 484 | float d2 = dist(x2,y2,fixed.x,fixed.y); 485 | float adiff = abs(a1-a2); 486 | float r = moveable.radius+fixed.radius+meshGap; 487 | float mountRatio; 488 | if (adiff > TWO_PI) 489 | adiff -= TWO_PI; 490 | if (adiff < .01) { // if rail is perpendicular to fixed circle 491 | mountRatio = (r-d1)/(d2-d1); 492 | // find position on line (if any) which corresponds to two radii 493 | } else if ( abs(x2-x1) < .01 ) { 494 | float m = 0; 495 | float c = (-m * y1 + x1); 496 | float aprim = (1 + m*m); 497 | float bprim = 2 * m * (c - fixed.x) - 2 * fixed.y; 498 | float cprim = fixed.y * fixed.y + (c - fixed.x) * (c - fixed.x) - r * r; 499 | float delta = bprim * bprim - 4*aprim*cprim; 500 | if (delta == 0) { 501 | println("zero delta"); 502 | } 503 | float my1 = (-bprim + sqrt(delta)) / (2 * aprim); 504 | float mx1 = m * my1 + c; 505 | float my2 = (-bprim - sqrt(delta)) / (2 * aprim); // use this if it's better 506 | float mx2 = m * my2 + c; 507 | // println("V x1,y1 " + x1/inchesToPoints + " " + y1/inchesToPoints + " x2,y2 " + x2/inchesToPoints + " " + y2/inchesToPoints + " fixed " + fixed.x/inchesToPoints + " " + fixed.y/inchesToPoints); 508 | // println(" aprim,bprim,cprim = " + aprim + " " + bprim + " " + cprim); 509 | // of the two spots which are best, and pick the one that is A) On the line and B) closest to the moveable gear's current position 510 | // println(" a m=" + m + " c=" + c + " aprim=" + aprim + " bprim=" + bprim + " cprim=" + cprim + " delta=" + delta); 511 | // println(" a mx1,mx2 " + mx1/inchesToPoints + " " + my1/inchesToPoints + " mx2,my2 " + mx2/inchesToPoints + " " + my2/inchesToPoints); 512 | if (my1 < min(y1,y2) || my1 > max(y1,y2) || 513 | dist(moveable.x,moveable.y,mx2,my2) < dist(moveable.x,moveable.y,mx1,mx2)) { 514 | // println(" swap"); 515 | mx1 = mx2; 516 | my1 = my2; 517 | } 518 | if (delta < 0) { 519 | mountRatio = -1; 520 | } else { 521 | mountRatio = dist(x1,y1,mx1,my1)/dist(x1,y1,x2,y2); 522 | } 523 | } else { // we likely have a gear on one of the lines on the left 524 | // given the line formed by x1,y1 x2,y2, find the two spots which are desiredRadius from fixed center. 525 | float m = (y2-y1)/(x2-x1); 526 | float c = (-m * x1 + y1); 527 | float aprim = (1 + m*m); 528 | float bprim = 2 * m * (c - fixed.y) - 2 * fixed.x; 529 | float cprim = fixed.x * fixed.x + (c - fixed.y) * (c - fixed.y) - r * r; 530 | float delta = bprim * bprim - 4*aprim*cprim; 531 | if (delta == 0) { 532 | println("zero delta"); 533 | } 534 | float mx1 = (-bprim + sqrt(delta)) / (2 * aprim); 535 | float my1 = m * mx1 + c; 536 | float mx2 = (-bprim - sqrt(delta)) / (2 * aprim); // use this if it's better 537 | float my2 = m * mx2 + c; 538 | // println("x1,y1 " + x1/inchesToPoints + " " + y1/inchesToPoints + " x2,y2 " + x2/inchesToPoints + " " + y2/inchesToPoints); 539 | // println(" aprim,bprim,cprim = " + aprim + " " + bprim + " " + cprim); 540 | // of the two spots which are best, and pick the one that is A) On the line and B) closest to the moveable gear's current position 541 | // println(" b m=" + m + " c=" + c + " aprim=" + aprim + " bprim=" + bprim + " cprim=" + cprim + " delta=" + delta); 542 | // println(" b mx1,mx2 " + mx1/inchesToPoints + " " + my1/inchesToPoints + " mx2,my2 " + mx2/inchesToPoints + " " + my2/inchesToPoints); 543 | if (mx1 < min(x1,x2) || mx1 > max(x1,x2) || my1 < min(y1,y2) || my1 > max(y1,y2) || 544 | dist(moveable.x,moveable.y,mx2,my2) < dist(moveable.x,moveable.y,mx1,mx2)) { 545 | // println(" swap"); 546 | mx1 = mx2; 547 | my1 = my2; 548 | } 549 | if (delta < 0) { 550 | mountRatio = -1; 551 | } else { 552 | mountRatio = dist(x1,y1,mx1,my1)/dist(x1,y1,x2,y2); 553 | } 554 | } 555 | if (mountRatio < 0 || mountRatio > 1 || Float.isNaN(mountRatio)) { 556 | loadError = 1; 557 | mountRatio = 0; 558 | // println("LOAD ERR"); 559 | } 560 | mountRatio = constrain(mountRatio,0,1); 561 | moveable.mount(this,mountRatio); 562 | } 563 | } 564 | 565 | class ArcRail implements Channel { 566 | float cx,cy, rad, begAngle, endAngle; 567 | ArcRail(float cx, float cy, float rad, float begAngle, float endAngle) { 568 | this.cx = cx*inchesToPoints; 569 | this.cy = cy*inchesToPoints; 570 | this.rad = rad*inchesToPoints; 571 | this.begAngle = begAngle; 572 | this.endAngle = endAngle; 573 | } 574 | 575 | PVector getPosition(float r) { 576 | float a = begAngle + (endAngle - begAngle)*r; 577 | return new PVector(cx+cos(a)*rad, cy+sin(a)*rad); 578 | } 579 | 580 | // fixed = 15.017775 6.61 1.5221801 581 | // moveable = 16.518276 2.237212 3.0443602 582 | // rail = 8.91 3.9100003 7.79 583 | // angles = -24.999998 15.0 584 | // desired r = 4.625595 585 | // d = 6.6779423 586 | // i1 12.791063 2.2343254 ang -23.352594 587 | // i2 16.517643 2.2343254 ang -12.421739 588 | 589 | 590 | void snugTo(Gear moveable, Gear fixed) { // get the movable gear mounted on this rail snug to the fixed gear 591 | 592 | // The fixed gear is surrounded by an imaginary circle which is the correct distance (r) away. 593 | // We need to intersect this with our arcRail circle, and find the intersection point which lies on the arcrail. 594 | // Mesh point 595 | // https://gsamaras.wordpress.com/code/determine-where-two-circles-intersect-c/ 596 | 597 | // println("Snug arcrail"); 598 | // println(" fixed = " + fixed.x/72 + " " + fixed.y/72 + " " + fixed.radius/72); 599 | // println(" moveable = " + moveable.x/72 + " " + moveable.y/72 + " " + moveable.radius/72); 600 | // println(" rail = " + cx/72 + " " + cy/72 + " " + rad/72); 601 | // println(" angles = " + degrees(begAngle) + " " + degrees(endAngle)); 602 | 603 | float x1 = fixed.x; 604 | float y1 = fixed.y; 605 | float r1 = moveable.radius+fixed.radius+meshGap; 606 | float x2 = this.cx; 607 | float y2 = this.cy; 608 | float r2 = this.rad; 609 | 610 | float d = dist(x1,y1,x2,y2); 611 | // println(" desired r = " + r1/72); 612 | // println(" d = " + d/72); 613 | 614 | if (d > r1+r2) { 615 | println(" circles are too far apart"); 616 | loadError = 1; 617 | return; 618 | } else if (abs(d) < .01 && abs(r1-r2) < .01) { 619 | println(" circles coincide"); 620 | loadError = 1; 621 | return; 622 | } else if (d + min(r1,r2) < max(r1,r2)) { 623 | println(" one circle contains the other"); 624 | loadError = 1; 625 | return; 626 | } 627 | float a = (r1*r1 - r2*r2 + d*d) / (2*d); 628 | float h = sqrt(r1*r1 - a*a); 629 | PVector p2 = new PVector( x1 + (a * (x2 - x1)) / d, 630 | y1 + (a * (y2 - y1)) / d); 631 | 632 | // these are our two intersection points (which may or may not fall on the arc) 633 | PVector i1 = new PVector( p2.x + (h * (y2 - y1))/ d, 634 | p2.y + (h * (x2 - x1))/ d); 635 | PVector i2 = new PVector( p2.x - (h * (y2 - y1))/ d, 636 | p2.y + (h * (x2 - x1))/ d); 637 | // println(" i1 " + i1.x/72 + " " + i1.y/72 + " ang " + degrees(atan2(i1.y-cy,i1.x-cx))); 638 | // println(" i2 " + i2.x/72 + " " + i2.y/72 + " ang " + degrees(atan2(i2.y-cy,i2.x-cx))); 639 | 640 | PVector best = i2; 641 | float ma = atan2(best.y-cy,best.x-cx); 642 | if (ma < begAngle || ma > endAngle) { 643 | best = i1; 644 | ma = atan2(best.y-cy,best.x-cx); 645 | if (ma < begAngle || ma > endAngle) { 646 | println("Intersection points don't fall on arc rail"); 647 | loadError = 1; 648 | return; 649 | } 650 | } 651 | float mountRatio = (ma-begAngle)/(endAngle-begAngle); 652 | if (mountRatio < 0 || mountRatio > 1) 653 | loadError = 1; 654 | moveable.mount(this, mountRatio); 655 | } 656 | 657 | void draw() { 658 | noFill(); 659 | stroke(110); 660 | strokeWeight(.23*inchesToPoints); 661 | arc(cx, cy, rad, rad, begAngle, endAngle); 662 | } 663 | } 664 | 665 | 666 | int[] rgTeeth = { // regular gears 667 | 30, 32, 34, 36, 40, 48, 50, 58, 60, 66, 72, 74, 80, 90, 94, 98, 100, 108, 668 | }; 669 | int [] ttTeeth = { // turntable gears 670 | 120, 144, 150 671 | }; 672 | 673 | class GearSetup { 674 | float notchStart; 675 | float notchEnd; 676 | int nbrLabels; 677 | int teeth; 678 | GearSetup(int teeth, float notchStart, float notchEnd, int nbrLabels) 679 | { 680 | this.teeth = teeth; 681 | this.notchStart = notchStart * inchesToPoints; 682 | this.notchEnd = notchEnd * inchesToPoints; 683 | this.nbrLabels = nbrLabels; 684 | } 685 | } 686 | 687 | HashMap gearSetups; 688 | 689 | void gearInit() 690 | { 691 | gearSetups = new HashMap(); 692 | gearSetups.put(108, new GearSetup(108, 0.4375, 3.0, 6)); 693 | gearSetups.put(100, new GearSetup(100, 0.40625, 2.8125, 5)); 694 | gearSetups.put( 98, new GearSetup( 98, 0.4375, 2.8125, 5)); 695 | gearSetups.put( 94, new GearSetup( 94, 0.375, 2.625, 5)); 696 | gearSetups.put( 90, new GearSetup( 90, 0.40625, 2.5, 5)); 697 | gearSetups.put( 80, new GearSetup( 80, 0.40625, 2.25, 4)); 698 | gearSetups.put( 74, new GearSetup( 74, 0.40625, 2.031, 4)); 699 | gearSetups.put( 72, new GearSetup( 72, 0.375, 2.0, 4)); 700 | gearSetups.put( 66, new GearSetup( 66, 0.375, 1.6875, 3)); 701 | gearSetups.put( 60, new GearSetup( 60, 0.3125, 1.625, 3)); 702 | gearSetups.put( 58, new GearSetup( 58, 0.3125, 1.5625, 3)); 703 | gearSetups.put( 50, new GearSetup( 50, 0.25, 1.3125, 2)); // notch joins axel 704 | gearSetups.put( 48, new GearSetup( 48, 0.375, 1.25, 2)); 705 | gearSetups.put( 40, new GearSetup( 40, 0.25, 1.0, 2)); // notch joins axel 706 | gearSetups.put( 36, new GearSetup( 36, 0.3125, 0.968, 1)); 707 | gearSetups.put( 34, new GearSetup( 34, 0.3125, 0.84375, 1)); 708 | gearSetups.put( 32, new GearSetup( 32, 0.3125, 0.8125, 1)); 709 | gearSetups.put( 30, new GearSetup( 30, 0.3125, 0.75, 1)); 710 | } 711 | 712 | class Gear implements Channel, Selectable { 713 | int teeth; 714 | int setupIdx; 715 | float radius; 716 | float rotation; 717 | float phase = 0; 718 | float x,y; 719 | float mountRatio = 0; 720 | boolean doFill = true; 721 | boolean showMount = true; 722 | boolean isMoving = false; // gear's position is moving 723 | boolean isFixed = false; // gear does not rotate or move 724 | boolean selected = false; 725 | boolean contributesToCycle = true; 726 | ArrayList meshGears; 727 | ArrayList stackGears; 728 | Channel itsChannel; 729 | String nom; 730 | GearSetup itsSetup; 731 | 732 | Gear(int teeth, int setupIdx, String nom) { 733 | this.itsSetup = gearSetups.get(teeth); 734 | this.teeth = teeth; 735 | this.nom = nom; 736 | this.setupIdx = setupIdx; 737 | this.radius = (this.teeth*toothRadius/PI); 738 | this.x = 0; 739 | this.y = 0; 740 | this.phase = 0; 741 | meshGears = new ArrayList(); 742 | stackGears = new ArrayList(); 743 | } 744 | 745 | boolean isClicked(int mx, int my) { 746 | return dist(mx, my, this.x, this.y) <= this.radius; 747 | } 748 | 749 | void unselect() { 750 | selected = false; 751 | } 752 | 753 | void select() { 754 | selected = true; 755 | } 756 | 757 | void nudge(int direction, int keycode) { 758 | int gearIdx = this.setupIdx; 759 | int teeth, oldTeeth; 760 | oldTeeth = this.teeth; 761 | if (isShifting) { 762 | teeth = setupTeeth[setupMode][gearIdx] + direction; 763 | } else { 764 | teeth = findNextTeeth(setupTeeth[setupMode][gearIdx], direction); 765 | } 766 | if (teeth < 24) { 767 | teeth = 151; 768 | } else if (teeth > 151) { 769 | teeth = 30; 770 | } 771 | setupTeeth[setupMode][gearIdx] = teeth; 772 | drawingSetup(setupMode, false); 773 | if (loadError != 0) { // disallow invalid meshes 774 | java.awt.Toolkit.getDefaultToolkit().beep(); 775 | setupTeeth[setupMode][gearIdx] = oldTeeth; 776 | drawingSetup(setupMode, false); 777 | } 778 | selectedObject = activeGears.get(gearIdx); 779 | selectedObject.select(); 780 | } 781 | 782 | 783 | int findNextTeeth(int teeth, int direction) { 784 | // println("Finding next tooth: " + teeth + " dir " + direction); 785 | int[] gTeeth = (this == turnTable? ttTeeth : rgTeeth); 786 | 787 | if (direction == 1) { 788 | for (int i = 0; i < gTeeth.length; ++i) { 789 | if (gTeeth[i] > teeth) 790 | return gTeeth[i]; 791 | } 792 | return gTeeth[0]; 793 | } else { 794 | for (int i = gTeeth.length-1; i >= 0; --i) { 795 | if (gTeeth[i] < teeth) 796 | return gTeeth[i]; 797 | } 798 | return gTeeth[gTeeth.length-1]; 799 | } 800 | } 801 | 802 | 803 | 804 | PVector getPosition(float r) { 805 | float d = notchToDist(r); // kGearLabelStart+(r-1)*kGearLabelIncr; 806 | return new PVector(x+cos(this.rotation+this.phase)*d, y+sin(this.rotation+this.phase)*d); 807 | } 808 | 809 | void meshTo(Gear parent) { 810 | parent.meshGears.add(this); 811 | 812 | // work out phase for gear meshing so teeth render interlaced 813 | float meshAngle = atan2(y-parent.y, x-parent.x); // angle where gears are going to touch (on parent gear) 814 | if (meshAngle < 0) 815 | meshAngle += TWO_PI; 816 | 817 | float iMeshAngle = meshAngle + PI; 818 | if (iMeshAngle >= TWO_PI) 819 | iMeshAngle -= TWO_PI; 820 | 821 | float parentMeshTooth = (meshAngle - parent.phase)*parent.teeth/TWO_PI; // tooth on parent, taking parent's phase into account 822 | 823 | // We want to insure that difference mod 1 is exactly .5 to insure a good mesh 824 | parentMeshTooth -= floor(parentMeshTooth); 825 | 826 | phase = (meshAngle+PI)+(parentMeshTooth+.5)*TWO_PI/teeth; 827 | } 828 | 829 | // Find position in our current channel which is snug to the fixed gear 830 | void snugTo(Gear anchor) { 831 | itsChannel.snugTo(this, anchor); 832 | } 833 | 834 | float notchToDist(float n) { 835 | return kGearLabelStart+(n-1)*kGearLabelIncr; 836 | } 837 | 838 | float distToNotch(float d) { 839 | return 1 + (d - kGearLabelStart)/kGearLabelIncr; 840 | } 841 | 842 | // Using this gear as the channel, find position for moveable gear which is snug to the fixed gear (assuming fixed gear is centered) 843 | void snugTo(Gear moveable, Gear fixed) { 844 | float d1 = 0; 845 | float d2 = radius; 846 | float d = moveable.radius+fixed.radius+meshGap; 847 | 848 | float mountRadDist = this.radius*d/d2; 849 | if (mountRadDist < 0 || mountRadDist > this.radius) 850 | loadError = 1; 851 | float mountNotch = distToNotch(mountRadDist); 852 | 853 | moveable.mount(this, mountNotch); 854 | // find position on line (if any) which corresponds to two radii 855 | } 856 | 857 | void stackTo(Gear parent) { 858 | parent.stackGears.add(this); 859 | this.x = parent.x; 860 | this.y = parent.y; 861 | this.phase = parent.phase; 862 | } 863 | 864 | void mount(Channel ch) { 865 | mount(ch, 0.0); 866 | } 867 | 868 | void recalcPosition() { // used for orbiting gears 869 | PVector pt = this.itsChannel.getPosition(this.mountRatio); 870 | this.x = pt.x; 871 | this.y = pt.y; 872 | } 873 | 874 | void mount(Channel ch, float r) { 875 | this.itsChannel = ch; 876 | this.mountRatio = r; 877 | PVector pt = ch.getPosition(r); 878 | this.x = pt.x; 879 | this.y = pt.y; 880 | // println("Gear " + teeth + " is at " + this.x/inchesToPoints + "," + this.y/inchesToPoints); 881 | } 882 | 883 | void crank(float pos) { 884 | if (!this.isFixed) { 885 | this.rotation = pos; 886 | float rTeeth = this.rotation*this.teeth; 887 | for (Gear mGear : meshGears) { 888 | mGear.crank(-(rTeeth)/mGear.teeth); 889 | } 890 | for (Gear sGear : stackGears) { 891 | sGear.crank(this.rotation); 892 | } 893 | if (isMoving) 894 | recalcPosition(); // technically only needed for orbiting gears 895 | } 896 | else { 897 | // this gear is fixed, but meshgears will rotate to the passed in pos 898 | for (Gear mGear : meshGears) { 899 | mGear.crank(pos + ( pos*this.teeth )/mGear.teeth); 900 | } 901 | } 902 | } 903 | 904 | void draw() { 905 | strokeWeight(1); 906 | strokeCap(ROUND); 907 | strokeJoin(ROUND); 908 | noFill(); 909 | stroke(0); 910 | 911 | pushMatrix(); 912 | translate(this.x, this.y); 913 | rotate(this.rotation+this.phase); 914 | 915 | float r1 = radius-.07*inchesToPoints; 916 | float r2 = radius+.07*inchesToPoints; 917 | float tAngle = TWO_PI/teeth; 918 | float tipAngle = tAngle*.1; 919 | 920 | if (doFill) { 921 | fill(220); 922 | } else { 923 | noFill(); 924 | } 925 | if (selected) { 926 | strokeWeight(4); 927 | stroke(64); 928 | } else { 929 | strokeWeight(0.5); 930 | stroke(128); 931 | } 932 | beginShape(); 933 | for (int i = 0; i < teeth; ++i) { 934 | float a1 = i*tAngle; 935 | float a2 = (i+.5)*tAngle; 936 | vertex(r2*cos(a1), r2*sin(a1)); 937 | vertex(r2*cos(a1+tipAngle), r2*sin(a1+tipAngle)); 938 | vertex(r1*cos(a2-tipAngle), r1*sin(a2-tipAngle)); 939 | vertex(r1*cos(a2+tipAngle), r1*sin(a2+tipAngle)); 940 | vertex(r2*cos(a1+tAngle-tipAngle), r2*sin(a1+tAngle-tipAngle)); 941 | vertex(r2*cos(a1+tAngle), r2*sin(a1+tAngle)); 942 | } 943 | endShape(); 944 | 945 | if (this == turnTable) { 946 | noStroke(); 947 | fill(255,192); 948 | beginShape(); 949 | for (int i = 0; i < 8; ++i) { 950 | vertex(kPaperRad*cos(i*TWO_PI/8), kPaperRad*sin(i*TWO_PI/8)); 951 | } 952 | endShape(); 953 | } 954 | 955 | strokeWeight(1); 956 | 957 | pushMatrix(); 958 | translate(0, radius-20); 959 | fill(127); 960 | textFont(gFont); 961 | textAlign(CENTER); 962 | text(""+teeth, 0, 0); 963 | noFill(); 964 | popMatrix(); 965 | 966 | if (showMount) { 967 | noStroke(); 968 | fill(192,128); 969 | ellipse(0, 0, kGearMountRadius, kGearMountRadius); 970 | 971 | pushMatrix(); 972 | float notchStart, notchEnd; 973 | int nbrLabels; 974 | if (itsSetup != null) { 975 | notchStart = itsSetup.notchStart; 976 | notchEnd = itsSetup.notchEnd; 977 | nbrLabels = itsSetup.nbrLabels; 978 | } else { 979 | // Make a guesstimate 980 | notchStart = max(radius*.1,16*seventyTwoScale); 981 | notchEnd = radius-max(radius*.1,8*seventyTwoScale); 982 | nbrLabels = 1 + int((notchEnd-notchStart-0.2*inchesToPoints)/(0.5*inchesToPoints)); 983 | } 984 | textFont(nFont); 985 | textAlign(CENTER); 986 | 987 | stroke(128); 988 | fill(128); 989 | int nbrNotches = (nbrLabels)*2-1; 990 | for (int i = 0; i < nbrNotches; ++i) { 991 | float x = kGearLabelStart + i * 0.25 * inchesToPoints; 992 | line(x,-(i % 2 == 0? kGearNotchHeightMaj : kGearNotchHeightMin), x, (i % 2 == 0? kGearNotchHeightMaj : kGearNotchHeightMin)); 993 | if (i % 2 == 0) { 994 | text((i/2)+1,x,kGearNotchHeightMaj+0.2*inchesToPoints); 995 | } 996 | } 997 | fill(192); 998 | noStroke(); 999 | rect(notchStart, -kGearNotchWidth/2, notchEnd-notchStart, kGearNotchWidth); 1000 | popMatrix(); 1001 | } 1002 | 1003 | 1004 | popMatrix(); 1005 | } 1006 | } 1007 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simulation of Cycloid Drawing Machine in Processing 2 | 3 | Physical machine designed by [Joe Freedman](https://kickstarter.com/projects/1765367532/cycloid-drawing-machine) and available on [Kickstarter](https://kickstarter.com/projects/1765367532/cycloid-drawing-machine). 4 | 5 | Processing simulation by [Jim Bumgardner](http://krazydad.com/about.php) 6 | 7 | ![Cycloid Drawing Machine](http://i.imgur.com/q4CFLI6.png "Cycloid Drawing Machine") 8 | 9 | ![Cycloid Drawing Machine](http://i.imgur.com/VC6v1op.png "Cycloid Drawing Machine") 10 | 11 | Try the live demo here: 12 | 13 | [krazydad.com/cdm/](http://krazydad.com/cdm/) 14 | 15 | This Processing (java) version has additional features, including the ability to create animated sequences. 16 | 17 | ![Animated sequence](http://i.imgur.com/mVS5xef.gif "Animated sequence") 18 | 19 | CDMS is licensed under the [MIT License](https://github.com/jbum/CycloidDrawingMachine/blob/master/LICENSE.txt) 20 | -------------------------------------------------------------------------------- /Utils.pde: -------------------------------------------------------------------------------- 1 | String saveFilename(String prefix) 2 | { 3 | String sf = prefix + year() + "-" + month() + "-" + day() + "_" + hour() + "." + minute() + "." + second() + (svgMode? ".svg" : ".png"); 4 | return sf; 5 | } 6 | 7 | void saveSnapshot(String prefix) 8 | { 9 | String sf = saveFilename(prefix); 10 | saveSnapshotAs(sf); 11 | println("Frame saved as " + sf); 12 | // Feedback 13 | for (Gear g : activeGears) { 14 | println("Gear " + g.nom + " has " + g.teeth + " teeth"); 15 | } 16 | } 17 | 18 | void saveSVGPathFragment() { 19 | println("Saving Path Fragment"); 20 | svgPathFragments += String.format("\n\n\n",((int)penColor) & 0x00FFFFFF,(float) penWidth,svgPath); 21 | svgPath = ""; 22 | } 23 | 24 | void saveSnapshotAs(String sf) 25 | { 26 | if (svgMode) { 27 | // println("Saving Path to SVG " + sf); 28 | saveSVGPathFragment(); 29 | PrintWriter fw; 30 | fw = createWriter(sf); 31 | fw.println(""); 32 | fw.println(""); 33 | fw.println(""); 34 | fw.write(svgPathFragments); 35 | //fw.println(String.format("",((int)penColor) & 0x00FFFFFF,(float) penWidth)); 36 | //fw.println(String.format("\n",svgPath)); 37 | fw.println(""); 38 | println(svgPath); 39 | fw.flush(); 40 | fw.close(); 41 | } else { 42 | PGraphics tmp = createGraphics(paper.width, paper.height); 43 | tmp.smooth(); 44 | tmp.beginDraw(); 45 | tmp.background(255); 46 | tmp.image(paper, 0, 0); 47 | tmp.endDraw(); 48 | tmp.save(sf); 49 | } 50 | } 51 | 52 | String getSetupString() 53 | { 54 | String ss = "Setup\t" + ((char) (65+ setupMode)) + "\n"; 55 | ss += "Gear Teeth\t"; 56 | for (int i = 0; i < setupTeeth[setupMode].length; ++i) { 57 | if (i > 0) ss += "\t"; 58 | ss += setupTeeth[setupMode][i]; 59 | } 60 | ss += "\nMount Points\t"; 61 | for (int i = 0; i < setupMounts[setupMode].length; ++i) { 62 | if (i > 0) ss += "\t"; 63 | ss += setupMounts[setupMode][i]; 64 | } 65 | ss += "\n"; 66 | ss += "Pen\t" + penRig.len + "\t" + penRig.angle + "°" + "\n"; 67 | return ss; 68 | } 69 | 70 | int GCD(int a, int b) { 71 | if (b==0) return a; 72 | return GCD(b,a%b); 73 | } 74 | 75 | 76 | // Compute total turntable rotations for current drawing 77 | int computeCyclicRotations() { 78 | int a = 1; // running minimum 79 | int idx = 0; 80 | for (Gear g : activeGears) { 81 | if (g.contributesToCycle && g != turnTable) { 82 | int ratioNom = turnTable.teeth; 83 | int ratioDenom = g.teeth; 84 | if (g.isMoving) { // ! cheesy hack for our orbit configuration, assumes anchorTable,anchorHub,orbit configuration 85 | ratioNom = turnTable.teeth * (activeGears.get(idx-1).teeth + g.teeth); 86 | ratioDenom = activeGears.get(idx-2).teeth * g.teeth; 87 | int gcd = GCD(ratioNom, ratioDenom); 88 | ratioNom /= gcd; 89 | ratioDenom /= gcd; 90 | } 91 | int b = min(ratioNom,ratioDenom) / GCD(ratioNom, ratioDenom); 92 | a = max(a,max(a,b)*min(a,b)/ GCD(a, b)); 93 | } 94 | idx += 1; 95 | } 96 | return a; 97 | } 98 | 99 | void invertConnectingRod() 100 | { 101 | if (selectedObject instanceof ConnectingRod) { 102 | ((ConnectingRod) selectedObject).invert(); 103 | } else if (activeConnectingRods.size() == 1) { 104 | activeConnectingRods.get(0).invert(); 105 | } else { 106 | println("Please select a connecting rod to invert"); 107 | } 108 | } 109 | 110 | void completeDrawing() 111 | { 112 | myFrameCount = 0; 113 | penRaised = true; 114 | int totalRotations = computeCyclicRotations(); 115 | if (!isTweening) 116 | println("Total turntable cycles needed = " + totalRotations); 117 | int framesPerRotation = int(TWO_PI / crankSpeed); 118 | myLastFrame = framesPerRotation * totalRotations + 1; 119 | passesPerFrame = isRecording? 10 : 360*2; // this sets up each pass to do 1 cycle 120 | isMoving = true; 121 | } 122 | 123 | void clearPaper() 124 | { 125 | svgPath = ""; 126 | svgPathFragments = ""; 127 | paper.beginDraw(); 128 | paper.clear(); 129 | paper.endDraw(); 130 | } 131 | 132 | void measureGears() { 133 | float[] sav = {0,0,0,0,0,0,0,0,0}; 134 | int i = 0; 135 | for (Gear g : activeGears) { 136 | sav[i] = g.rotation; 137 | i++; 138 | } 139 | myFrameCount += 1; 140 | turnTable.crank(myFrameCount*crankSpeed); // The turntable is always the root of the propulsion chain, since it is the only required gear. 141 | i = 0; 142 | for (Gear g : activeGears) { 143 | sav[i] = g.rotation; 144 | i++; 145 | // Turntable should be crankSpeed 146 | println(g.teeth + ": " + (g.rotation - sav[i])/crankSpeed); 147 | } 148 | 149 | } 150 | 151 | void nudge(int direction, int kc) 152 | { 153 | if (selectedObject != null) { 154 | selectedObject.nudge(direction, kc); 155 | } 156 | } 157 | 158 | void deselect() { 159 | if (selectedObject != null) { 160 | selectedObject.unselect(); 161 | selectedObject = null; 162 | } 163 | } 164 | 165 | void advancePenColor(int direction) { 166 | saveSVGPathFragment(); 167 | penColorIdx = (penColorIdx + penColors.length + direction) % penColors.length; 168 | penColor = penColors[penColorIdx]; 169 | paper.beginDraw(); 170 | paper.stroke(penColor); 171 | paper.endDraw(); 172 | println("Pen color changed"); 173 | } 174 | 175 | void advancePenWidth(int direction) { 176 | saveSVGPathFragment(); 177 | penWidthIdx = (penWidthIdx + penWidths.length + direction) % penWidths.length; 178 | penWidth = penWidths[penWidthIdx]; 179 | paper.beginDraw(); 180 | paper.strokeWeight(penWidth); 181 | paper.endDraw(); 182 | println("Pen width set to " + penWidth); 183 | } 184 | 185 | void toggleOutputmode() 186 | { 187 | svgMode = !svgMode; 188 | if (svgMode) { 189 | println("SVG ON - saved frames will be in SVG format"); 190 | } else { 191 | println("SVG OFF - saved frames will be in PNG format"); 192 | } 193 | } 194 | 195 | void toggleHiresmode() 196 | { 197 | hiresMode = !hiresMode; 198 | PGraphics oldPaper = paper; 199 | if (hiresMode) { 200 | paperScale = 2; 201 | println("Hires ON - saved frames are twice the size, drawings are 4 times as slow"); 202 | } 203 | else { 204 | paperScale = 1; 205 | println("Hires OFF"); 206 | } 207 | paperWidth = 9*inchesToPoints*paperScale; 208 | paper = createGraphics(int(paperWidth), int(paperWidth)); 209 | paper.smooth(8); 210 | clearPaper(); 211 | paper.beginDraw(); 212 | paper.image(oldPaper,0,0,paper.width,paper.height); 213 | paper.endDraw(); 214 | penRaised = true; 215 | } 216 | 217 | 218 | 219 | 220 | void saveSettings() { 221 | // default filename "setup_" + (char)('A' + setupMode) + "_override.txt" 222 | selectOutput("Select a file to save settings to:", "saveCallback"); 223 | // println("Oname = " + oName); 224 | } 225 | 226 | void saveCallback(File fileSelection) 227 | { 228 | if (fileSelection == null) { 229 | println("Canceled"); 230 | return; 231 | } 232 | JSONObject settings = new JSONObject(); 233 | settings.setInt("layout", setupMode); 234 | 235 | JSONArray gears = new JSONArray(); 236 | for (int i = 0; i < setupTeeth[setupMode].length; ++i) { 237 | gears.setInt(i, setupTeeth[setupMode][i]); 238 | } 239 | settings.setJSONArray("gears", gears); 240 | 241 | JSONArray mounts = new JSONArray(); 242 | for (int i = 0; i < setupMounts[setupMode].length; ++i) { 243 | mounts.setFloat(i, setupMounts[setupMode][i]); 244 | } 245 | settings.setJSONArray("mounts", mounts); 246 | 247 | JSONArray invs = new JSONArray(); 248 | for (int i = 0; i < setupInversions[setupMode].length; ++i) { 249 | invs.setBoolean(i, setupInversions[setupMode][i]); 250 | } 251 | settings.setJSONArray("inversions", invs); 252 | 253 | JSONObject penRigSetup = new JSONObject(); 254 | penRigSetup.setFloat("length", setupPens[setupMode][0]); 255 | penRigSetup.setFloat("angle", setupPens[setupMode][1]); 256 | settings.setJSONObject("penrig", penRigSetup); 257 | 258 | saveJSONObject(settings, fileSelection.getAbsolutePath()); 259 | } 260 | 261 | void loadSettings() 262 | { 263 | selectInput("Select a file to load settings from:", "loadCallback"); 264 | 265 | } 266 | 267 | void loadCallback(File fileSelection) 268 | { 269 | if (fileSelection == null) { 270 | println("Canceled"); 271 | return; 272 | } 273 | JSONObject settings = loadJSONObject(fileSelection.getAbsolutePath()); 274 | if (settings == null) { 275 | println("Invalid settings file"); 276 | return; 277 | } 278 | setupMode = settings.getInt("layout"); 279 | 280 | JSONArray gears = settings.getJSONArray("gears"); 281 | if (gears != null) { 282 | int ng = min(setupTeeth[setupMode].length, gears.size()); 283 | for (int i = 0; i < ng; ++i) { 284 | setupTeeth[setupMode][i] = gears.getInt(i); 285 | } 286 | } 287 | 288 | JSONArray mounts = settings.getJSONArray("mounts"); 289 | if (mounts != null) { 290 | int nm = min(setupMounts[setupMode].length, mounts.size()); 291 | for (int i = 0; i < nm; ++i) { 292 | setupMounts[setupMode][i] = mounts.getFloat(i); 293 | } 294 | } 295 | 296 | JSONArray invs = settings.getJSONArray("inversions"); 297 | if (invs != null) { 298 | int nm = min(setupInversions[setupMode].length, invs.size()); 299 | for (int i = 0; i < nm; ++i) { 300 | setupInversions[setupMode][i] = invs.getBoolean(i); 301 | } 302 | } 303 | 304 | JSONObject penRigSetup = settings.getJSONObject("penrig"); 305 | if (penRigSetup != null) { 306 | setupPens[setupMode][0] = penRigSetup.getFloat("length"); 307 | setupPens[setupMode][1] = penRigSetup.getFloat("angle"); 308 | } 309 | 310 | deselect(); 311 | drawingSetup(setupMode, false); 312 | } 313 | 314 | void drawFulcrumLabels() { 315 | textFont(nFont); 316 | textAlign(CENTER); 317 | fill(64); 318 | stroke(92); 319 | strokeWeight(0.5); 320 | pushMatrix(); 321 | translate(3.1*inchesToPoints, 10.23*inchesToPoints); 322 | rotate(PI/2); 323 | int nbrNotches = 39; 324 | float startNotch = 0.25*inchesToPoints; 325 | float notchIncr = 0.25*inchesToPoints; 326 | float minNotch = 0.9*inchesToPoints; 327 | float lilNotch = minNotch/2; 328 | float widIncr = 1.722*inchesToPoints/nbrNotches; 329 | float notchSize = minNotch; 330 | float notchX = -startNotch; 331 | for (int n = 0; n < 39; ++n) { 332 | line(notchX,0,notchX,n % 2 == 1? notchSize : lilNotch); 333 | if (n % 2 == 1) { 334 | text("" + (n/2+1),notchX,lilNotch); 335 | } 336 | notchSize += widIncr; 337 | notchX -= notchIncr; 338 | } 339 | popMatrix(); 340 | 341 | } 342 | -------------------------------------------------------------------------------- /data/Notch-Font.vlw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbum/CycloidDrawingMachine/65be91e873594233d0aa9635cffa32dbd862e46e/data/Notch-Font.vlw -------------------------------------------------------------------------------- /data/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbum/CycloidDrawingMachine/65be91e873594233d0aa9635cffa32dbd862e46e/data/title.png -------------------------------------------------------------------------------- /data/title_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbum/CycloidDrawingMachine/65be91e873594233d0aa9635cffa32dbd862e46e/data/title_dark.png -------------------------------------------------------------------------------- /setups/Pattern A 150_72.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 0, 4 | 3.383878707885742, 5 | 10.625 6 | ], 7 | "inversions": [true], 8 | "layout": 0, 9 | "penrig": { 10 | "length": 7.125, 11 | "angle": -55 12 | }, 13 | "gears": [ 14 | 150, 15 | 72 16 | ] 17 | } -------------------------------------------------------------------------------- /setups/Pattern B 30_120-98.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 0.8266160488128662, 4 | 4.216946125030518, 5 | 9.375 6 | ], 7 | "inversions": [true], 8 | "layout": 1, 9 | "penrig": { 10 | "length": 5.625, 11 | "angle": -70 12 | }, 13 | "gears": [ 14 | 120, 15 | 94, 16 | 98, 17 | 30 18 | ] 19 | } -------------------------------------------------------------------------------- /setups/Pattern B 30_120_98.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 0.8266160488128662, 4 | 4.216946125030518, 5 | 9.375 6 | ], 7 | "inversions": [true], 8 | "layout": 1, 9 | "penrig": { 10 | "length": 6, 11 | "angle": -80 12 | }, 13 | "gears": [ 14 | 120, 15 | 94, 16 | 98, 17 | 30 18 | ] 19 | } -------------------------------------------------------------------------------- /setups/Pattern B 30_150_98.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 0.8266160488128662, 4 | 4.216946125030518, 5 | 9.375 6 | ], 7 | "inversions": [true], 8 | "layout": 1, 9 | "penrig": { 10 | "length": 1, 11 | "angle": -35 12 | }, 13 | "gears": [ 14 | 150, 15 | 94, 16 | 98, 17 | 30 18 | ] 19 | } -------------------------------------------------------------------------------- /setups/Pattern B 34_120_90.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 1.5, 4 | 4.4798479080200195, 5 | 10 6 | ], 7 | "inversions": [false], 8 | "layout": 1, 9 | "penrig": { 10 | "length": 6, 11 | "angle": 90 12 | }, 13 | "gears": [ 14 | 120, 15 | 94, 16 | 90, 17 | 34 18 | ] 19 | } -------------------------------------------------------------------------------- /setups/Pattern B 58_150_90.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 2.5, 4 | 4.4798479080200195, 5 | 10 6 | ], 7 | "inversions": [false], 8 | "layout": 1, 9 | "penrig": { 10 | "length": 7.625, 11 | "angle": 100 12 | }, 13 | "gears": [ 14 | 150, 15 | 94, 16 | 90, 17 | 58 18 | ] 19 | } -------------------------------------------------------------------------------- /setups/Pattern B 58_150_90_b.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 2.5, 4 | 2.7298479080200195, 5 | 10 6 | ], 7 | "inversions": [false], 8 | "layout": 1, 9 | "penrig": { 10 | "length": 7.625, 11 | "angle": 100 12 | }, 13 | "gears": [ 14 | 150, 15 | 94, 16 | 90, 17 | 58 18 | ] 19 | } -------------------------------------------------------------------------------- /setups/Pattern B 72_150_90.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 2.5, 4 | 4.4798479080200195, 5 | 10 6 | ], 7 | "inversions": [false], 8 | "layout": 1, 9 | "penrig": { 10 | "length": 7.625, 11 | "angle": 100 12 | }, 13 | "gears": [ 14 | 150, 15 | 94, 16 | 90, 17 | 72 18 | ] 19 | } -------------------------------------------------------------------------------- /setups/Pattern B 74_150_90.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 2.5, 4 | 4.4798479080200195, 5 | 10 6 | ], 7 | "inversions": [false], 8 | "layout": 1, 9 | "penrig": { 10 | "length": 6.625, 11 | "angle": 90 12 | }, 13 | "gears": [ 14 | 150, 15 | 94, 16 | 90, 17 | 74 18 | ] 19 | } -------------------------------------------------------------------------------- /setups/Pattern C 150_40_30.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 0.8973000049591064, 4 | 1.5, 5 | 12 6 | ], 7 | "inversions": [false], 8 | "layout": 2, 9 | "penrig": { 10 | "length": 1.375, 11 | "angle": -80 12 | }, 13 | "gears": [ 14 | 150, 15 | 50, 16 | 100, 17 | 30, 18 | 40 19 | ] 20 | } -------------------------------------------------------------------------------- /setups/Pattern C 150_40_34.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 0.8973000049591064, 4 | 1.5, 5 | 12 6 | ], 7 | "inversions": [false], 8 | "layout": 2, 9 | "penrig": { 10 | "length": 3, 11 | "angle": -90 12 | }, 13 | "gears": [ 14 | 150, 15 | 50, 16 | 100, 17 | 34, 18 | 40 19 | ] 20 | } -------------------------------------------------------------------------------- /setups/Pattern D 144_72_100.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 4, 4 | 4, 5 | 0.800000011920929, 6 | 2, 7 | 8.625 8 | ], 9 | "inversions": [ 10 | false, 11 | false 12 | ], 13 | "layout": 3, 14 | "penrig": { 15 | "length": 5.75, 16 | "angle": -65 17 | }, 18 | "gears": [ 19 | 144, 20 | 100, 21 | 72 22 | ] 23 | } -------------------------------------------------------------------------------- /setups/Pattern F 120_74_100.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 0.699999988079071, 4 | 3.383878707885742, 5 | 4, 6 | 0.21000003814697266, 7 | 12.75, 8 | 5.5, 9 | 4.125 10 | ], 11 | "inversions": [ 12 | false, 13 | false, 14 | false 15 | ], 16 | "layout": 5, 17 | "penrig": { 18 | "length": 5.75, 19 | "angle": -110 20 | }, 21 | "gears": [ 22 | 120, 23 | 100, 24 | 74 25 | ] 26 | } -------------------------------------------------------------------------------- /setups/Pattern F 150_74_100_vb.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 0.699999988079071, 4 | 3.383878707885742, 5 | 4, 6 | 0.21000003814697266, 7 | 12.75, 8 | 5.5, 9 | 5.25 10 | ], 11 | "inversions": [ 12 | false, 13 | false, 14 | false 15 | ], 16 | "layout": 5, 17 | "penrig": { 18 | "length": 7.375, 19 | "angle": -65 20 | }, 21 | "gears": [ 22 | 150, 23 | 100, 24 | 74 25 | ] 26 | } -------------------------------------------------------------------------------- /setups/Pattern G 50_150_40_34.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 2.5, 4 | 1, 5 | 14 6 | ], 7 | "inversions": [false], 8 | "layout": 6, 9 | "penrig": { 10 | "length": 4, 11 | "angle": -90 12 | }, 13 | "gears": [ 14 | 150, 15 | 50, 16 | 100, 17 | 34, 18 | 40, 19 | 50, 20 | 50 21 | ] 22 | } -------------------------------------------------------------------------------- /setups/Pattern G 72_144_90_34_36.txt: -------------------------------------------------------------------------------- 1 | { 2 | "mounts": [ 3 | 2.5, 4 | 1, 5 | 14.125 6 | ], 7 | "inversions": [false], 8 | "layout": 6, 9 | "penrig": { 10 | "length": 4, 11 | "angle": -90 12 | }, 13 | "gears": [ 14 | 144, 15 | 50, 16 | 90, 17 | 36, 18 | 34, 19 | 50, 20 | 72 21 | ] 22 | } -------------------------------------------------------------------------------- /sketch.properties: -------------------------------------------------------------------------------- 1 | mode.id=processing.mode.java.JavaMode 2 | mode=Java 3 | --------------------------------------------------------------------------------