Last active
March 15, 2018 16:44
-
-
Save melsov/3f52b7fc62be16bdc3fbe64b1e10775e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math | |
import turtle | |
epsilonMM = 0.001 | |
epsilonSlope = 0.0000001 | |
class Vector2(object): | |
def __init__(self, x=None, y=None): | |
if y is None: | |
if x is None: | |
x = 0 | |
y = x | |
self.x = float(x) | |
self.y = float(y) | |
def __add__(self, other): | |
if isinstance(other, Vector2): | |
return Vector2(self.x + other.x, self.y + other.y) | |
else: | |
return Vector2(self.x + other, self.y + other) | |
def __sub__(self, other): | |
return Vector2(self.x - other.x, self.y - other.y) | |
def __mul__(self, other): | |
if isinstance(other, Vector2): | |
return Vector2(self.x * other.x, self.y * other.y) | |
else: | |
return Vector2(self.x * other, self.y * other) | |
def __div__(self, other): | |
if isinstance(other, Vector2): | |
return Vector2(self.x / other.x, self.y / other.y) | |
else: | |
return Vector2(self.x / other, self.y / other) | |
def average(self, other): | |
return Vector2((self.x + other.x) / 2.0, (self.y + other.y) / 2.0) | |
def _epsilonEqual(self, a, b): | |
return abs(a - b) < epsilonMM | |
def isEpsilonZero(self): | |
return abs(self.x) < epsilonMM and abs(self.y) < epsilonMM | |
def __eq__(self, other): | |
return (self - other).isEpsilonZero() | |
def max(self, other): | |
return Vector2(self.x if self.x > other.x else other.x, self.y if self.y > other.y else other.y) | |
def min(self, other): | |
return Vector2(self.x if self.x < other.x else other.x, self.y if self.y < other.y else other.y) | |
def lengthSquared(self): | |
return self.x * self.x + self.y + self.y | |
def length(self): | |
return math.sqrt(self.lengthSquared()) | |
def normalized(self): | |
l = self.length() | |
return Vector2(self.x / l, self.y / l) | |
def max(self, other): | |
return Vector2(max(self.x, other.x), max(self.y, other.y)) | |
def min(self, other): | |
return Vector2(min(self.x, other.x), min(self.y, other.y)) | |
def dot(self, other): | |
return self.x * other.x + self.y + other.y | |
def slope(self): | |
if self.x == 0.0: | |
if self.y != 0.0: | |
return math.copysign(math.pow(1.6, 300), self.y) | |
else: | |
return 1.0 | |
return self.y / self.x | |
def clone(self): | |
return Vector2(self.x, self.y) | |
def toZeroOneNegPos(self): | |
xx = 0.0; | |
yy = 0.0 | |
if self.x > 0.0: | |
xx = 1.0 | |
if self.y > 0.0: | |
yy = 1.0 | |
return Vector2(xx, yy) | |
def __str__(self): | |
return "v2 x: %d, y: %d" % (self.x, self.y) | |
class Line2(object): | |
def __init__(self, slope, intercept): | |
self.slope = float(slope) | |
self.intercept = float(intercept) | |
def eval(self, x): | |
return self.slope * x + self.intercept | |
def pointAtX(self, x): | |
return Vector2(x, self.eval(x)) | |
def inverse(self): | |
if abs(self.slope) < epsilonSlope: | |
return Line2(math.pow(10, 300), 1) | |
return Line2(1.0 / self.slope, -self.intercept / self.slope) | |
def evalY(self, y): | |
if abs(self.slope) < epsilonSlope: | |
return math.copysign(math.pow(10, 300), y) | |
return (y - self.intercept) / self.slope | |
def pointAtY(self, y): | |
return Vector2(self.evalY(y), y) | |
def aboveOrBelow(self, vec2): | |
if vec2.y > self.eval(vec2.x): | |
return 1 | |
if vec2.y < self.eval(vec2.x): | |
return -1 | |
return 0 | |
def FromVectors(self, vA, vB): | |
s = (vA - vB).slope | |
return Line2(s, vA.y - s * vA.x) | |
def __str__(self): | |
return "L: slope: %d | cept: %d" % (self.slope, self.intercept) | |
class Box2(object): | |
def __init__(self, min, max): | |
self.lowerLeft = min | |
self.upperRight = max | |
def clone(self): | |
return Box2(self.lowerLeft, self.upperRight) | |
def getCornerInDirection(self, vDirection): | |
direction = vDirection.toZeroOneNegPos() | |
return (self.upperRight * direction) + (self.lowerLeft * ((direction * -1) + 1)) | |
def getClosestCorner(self, v): | |
print("v - center ", str((v - self.center()))) | |
return self.getCornerInDirection(v - self.center()) | |
def corners(self): | |
return self.lowerLeft, self.getCornerInDirection(Vector2(1, -1)), self.upperRight, self.getCornerInDirection( | |
Vector2(-1, 1)) | |
def size(self): | |
return self.upperRight - self.lowerLeft | |
def expandWith(self, v): | |
self.lowerLeft = self.lowerLeft.min(v) | |
self.upperRight = self.upperRight.max(v) | |
def center(self): | |
return self.lowerLeft.average(self.upperRight) | |
def isInside(self, v, epsilon=0.00001): | |
return self.isInsideX(v.x, epsilon) and self.isInsideY(v.y, epsilon) | |
def isInsideX(self, x, epsilon=0.00001): | |
return self.lowerLeft.x < x + epsilon and x - epsilon < self.upperRight.x | |
def isInsideY(self, y, epsilon=0.00001): | |
return self.lowerLeft.y < y + epsilon and y - epsilon < self.upperRight.y | |
def isOutsideXAndY(self, v, epsilon=0.00001): | |
return not self.isInsideX(v.x, epsilon) and not self.isInsideY(v.y, epsilon) | |
def isIntersectedByLine(self, line): | |
sides = [] | |
for c in self.corners(): | |
ab = line.aboveOrBelow(c) | |
if ab == 0: | |
return True | |
if len(sides) > 0: | |
for side in sides: | |
if side != ab: | |
return True | |
sides.append(ab) | |
return False | |
def intersectingPoints(self, line): | |
result = [] | |
points = (line.pointAtX(self.lowerLeft.x), line.pointAtX(self.upperRight.x), line.pointAtY(self.lowerLeft.y), | |
line.pointAtY(self.upperRight.y)) | |
for p in points: | |
if self.isInside(p): | |
result.append(p) | |
return result | |
def getClosestBorderPoint(self, segmentA, segmentB): | |
aIsInside = self.isInside(segmentA) | |
bIsInside = self.isInside(segmentB) | |
if bIsInside and not aIsInside: | |
vFrom = segmentB | |
vDestination = segmentA | |
else: | |
vFrom = segmentA | |
vDestination = segmentB | |
dif = vDestination - vFrom | |
s = dif.slope() | |
a = vFrom.y - s * vFrom.x | |
line = Line2(s, a) | |
if aIsInside or bIsInside: | |
corner = self.getCornerInDirection(dif) | |
if abs(dif.x) < epsilonSlope: | |
return Vector2(vFrom.x, corner.y) | |
if abs(dif.y) < epsilonSlope: | |
return Vector2(corner.x, vFrom.y) | |
ab = line.aboveOrBelow(corner) * math.copysign(1, s * dif.x) | |
if ab == 1: | |
return Vector2(corner.x, s * corner.x + a) | |
if abs(s) < epsilonSlope: | |
return Vector2(vFrom.x, corner.y) | |
else: | |
return Vector2((corner.y - a) / s, corner.y) | |
else: # neither a or b inside. CONSIDER: deal with this case less crudely | |
corner = self.getClosestCorner(vDestination) | |
if self.isOutsideXAndY(vDestination): | |
return corner | |
elif self.isInsideX(vDestination.x): | |
return Vector2(vDestination.x, corner.y) | |
else: | |
return Vector2(corner.x, vDestination.y) | |
class MachineMove(object): | |
def __init__(self, vPos, penDown=False): | |
self.position = vPos | |
self.penDown = penDown | |
def clone(self): | |
return MachineMove(self.position.clone(), self.penDown) | |
class GCTurtle(turtle.Turtle): | |
_header = """ | |
% | |
(Header) | |
(Generated by GCTurtle.) | |
M3 | |
(Header end.) | |
G21 (All units in mm) | |
G90 (Specifying absolute positions) | |
""" | |
_footer = """ | |
(Footer) | |
M5 | |
G00 X0 Y0 | |
M2 | |
(Using default footer. To add your own footer create file "footer" in the output dir.) | |
(end) | |
% | |
""" | |
_zTravelHeight = 5.0 | |
_zPenetrateDepth = -1.0 | |
_homeXY = Vector2(1, 1) | |
_paperSize = Vector2(8.5 * 25.4 - 3, 11 * 25.4 - 3) | |
def __init__(self): | |
turtle.Turtle.__init__(self) | |
self._penetrateSpeed = 100 | |
self._drawSpeed = 1500 | |
self.gcode = "" | |
self.prevPosition = self._getPosition() | |
self.prevPenPosition = 0.0 | |
self.shouldAddDrawSpeed = False | |
self.paper = Box2(self._homeXY, self._homeXY + self._paperSize) | |
self._shouldScaleToPaper = True | |
self.maxTravelBox = self.paper.clone() | |
self.machineMoves = [] | |
self.alreadyAdjustedMoves = False | |
self.fileName = "turtle.gcode" | |
self.lines = [] | |
#self.outFile = open(self.fileName, "w") | |
self._constructHeader() | |
def _getPosition(self): | |
return Vector2(self.position()[0], self.position()[1]) | |
def _appendGCode(self, s): | |
self.lines.append(s) | |
# self.outFile.write(s) | |
# self.gcode += s | |
def _constructHeader(self): | |
s = self._header | |
s += "\n(move to home position) \n" | |
s += self._liftPen() | |
s += self._travelMove(self._homeXY) | |
s += "\n(begin drawing)\n" | |
self._appendGCode(s) | |
def _liftPen(self): | |
self.prevPenPosition = self._zTravelHeight | |
return "G00 Z %d \n" % self._zTravelHeight | |
def _lowerPen(self): | |
self.shouldAddDrawSpeed = True | |
self.prevPenPosition = self._zPenetrateDepth | |
return "G01 Z %d F %d \n" % (self._zPenetrateDepth, self._penetrateSpeed) | |
def _travelMove(self, v): | |
# TODO: keep movement on paper | |
return "G00 X %d Y %d \n" % (v.x, v.y) | |
def _drawMove(self, v): | |
result = "G01 X %d Y %d" % (v.x, v.y) | |
if self.shouldAddDrawSpeed: | |
self.shouldAddDrawSpeed = False | |
result = "%s F %d" % (result, self._drawSpeed) | |
result = "%s\n" % result | |
return result | |
def _isPrevPenDown(self): | |
return self.prevPenPosition < self._zPenetrateDepth + .01 | |
def _updatePen(self, nextMove, prevPenDown): | |
if nextMove.penDown and not prevPenDown: | |
# if self.isdown() and not self._isPrevPenDown(): | |
self._appendGCode(self._lowerPen()) | |
elif not nextMove.penDown and prevPenDown: | |
# elif not self.isdown() and self._isPrevPenDown(): | |
self._appendGCode(self._liftPen()) | |
def _moveXY(self): | |
p = self._getPosition() | |
if p == self.prevPosition: | |
return | |
self.maxTravelBox.expandWith(p) | |
self.machineMoves.append(MachineMove(p, self.isdown())) | |
self.prevPosition = p | |
def _writeMove(self, mm, prevPenDown): | |
self._updatePen(mm, prevPenDown) | |
if mm.penDown: | |
self._appendGCode(self._drawMove(mm.position)) | |
else: | |
self._appendGCode(self._travelMove(mm.position)) | |
def _updateGCode(self): | |
# self._updatePen() | |
self._moveXY() | |
# override turtle methods | |
def forward(self, distance): | |
turtle.Turtle.forward(self, distance) | |
self._updateGCode() | |
def fd(self, distance): | |
self.forward(distance) | |
def back(self, distance): | |
turtle.Turtle.back(self, distance) | |
self._updateGCode() | |
def setpos(self, x, y): | |
turtle.Turtle.setpos(self, x, y) | |
self._updateGCode() | |
def setposition(self, x, y): | |
self.setpos(x, y) | |
def goto(self, x, y=None): | |
self.setpos(x, y) | |
def setx(self, x): | |
turtle.Turtle.setx(self, x) | |
self._updateGCode() | |
def sety(self, y): | |
turtle.Turtle.sety(self, y) | |
self._updateGCode() | |
def home(self): | |
turtle.Turtle.home() | |
self._updateGCode() | |
def pendown(self): | |
turtle.Turtle.pendown(self) | |
self._updateGCode() | |
def pd(self): | |
self.pendown() | |
def down(self): | |
self.pendown() | |
def penup(self): | |
turtle.Turtle.penup(self) | |
self._updateGCode() | |
def pu(self): | |
self.penup() | |
def up(self): | |
self.penup() | |
# end of turtle methods | |
def drawPaper(self, color="#CCCCCC"): | |
start = self.position() | |
startColor = self.pencolor() | |
penWasDown = self.isdown() | |
turtle.Turtle.penup(self) | |
corners = self.paper.corners() | |
self.setpos(corners[0].x, corners[0].y) | |
turtle.Turtle.pendown(self) | |
turtle.Turtle.color(self, color) | |
for i in range(1, 5): | |
turtle.Turtle.setpos(self, corners[i % 4].x, corners[i % 4].y) | |
self.color(startColor) | |
turtle.Turtle.penup(self) | |
turtle.Turtle.setpos(self, start[0], start[1]) | |
if not penWasDown: | |
turtle.Turtle.penup(self) | |
def scaleMachineMoves(self): | |
vpropo = self.paper.size() / self.maxTravelBox.size() | |
propo = vpropo.x if vpropo.x < vpropo.y else vpropo.y | |
propo = propo if propo < 1 else 1 | |
for i in range(len(self.machineMoves)): | |
self.machineMoves[i] = MachineMove( | |
((self.machineMoves[i].position - self.maxTravelBox.lowerLeft) * propo) + self.paper.lowerLeft, | |
self.machineMoves[i].penDown) | |
def truncateMachineMoves(self): | |
self.machineMoves[:] = list( | |
self.getTruncatedMoves()) # [m for m in self.machineMoves if self.paper.isInside(m.position)] | |
# FIXME: dicey behaviour? | |
def getTruncatedMoves(self): | |
lastInside = None | |
lastOutside = None | |
for i in range(len(self.machineMoves)): | |
move = self.machineMoves[i] | |
if self.paper.isInside(move.position): | |
lastInside = move.clone() | |
if lastOutside != None: | |
yield MachineMove(self.paper.getClosestBorderPoint(lastOutside.position, move.position), | |
move.penDown) | |
lastOutside = None | |
yield move | |
else: | |
lastOutside = move.clone() | |
if lastInside != None: | |
yield MachineMove(self.paper.getClosestBorderPoint(lastInside.position, move.position), | |
move.penDown) | |
lastInside = None | |
def writeMachineMoves(self): | |
prevPenDown = False | |
for i in range(0, len(self.machineMoves)): | |
self._writeMove(self.machineMoves[i], prevPenDown) | |
prevPenDown = self.machineMoves[i].penDown | |
def adjustMachineMoves(self): | |
if self.alreadyAdjustedMoves: | |
return | |
if self._shouldScaleToPaper: | |
self.scaleMachineMoves() | |
else: | |
self.truncateMachineMoves() | |
self.alreadyAdjustedMoves = True | |
def showMachineMoves(self): | |
self.adjustMachineMoves() | |
turtle.Turtle.color(self, "#CCCC99") | |
turtle.Turtle.penup(self) | |
turtle.Turtle.setpos(self, self.machineMoves[0].position.x, self.machineMoves[0].position.y) | |
turtle.Turtle.pendown(self) | |
for m in self.machineMoves: | |
turtle.Turtle.setpos(self, m.position.x, m.position.y) | |
def printGCode(self): | |
self.adjustMachineMoves() | |
self.writeMachineMoves() | |
self._appendGCode(self._footer) | |
# print(self.gcode) | |
# print(self._footer) | |
# self.outFile.close() | |
# f = open(self.fileName, "r") | |
# lines = f.readlines() | |
for line in self.lines: | |
print line.strip() | |
# experiment w. turtle functions | |
def drawIsoscelesTri(w, baseAng): | |
baseAng = max(min(baseAng, 89), 0) | |
peakAng = 180 - 2 * baseAng | |
h = w / 2 / math.cos(math.radians(baseAng)) | |
h = math.sqrt((w / 2) * (w / 2) + h * h) | |
buddy.forward(h) | |
buddy.right(180 - baseAng) | |
buddy.forward(w) | |
buddy.right(180 - baseAng) | |
buddy.forward(h) | |
buddy.right(180 - peakAng) | |
def pizza(rad, slices): | |
peakAng = 360.0 / float(slices) | |
w2 = rad * math.sin(math.radians(peakAng / 2.0)) | |
for i in range(slices): | |
drawIsoscelesTri(w2 * 2, 90.0 - peakAng / 2.0) | |
buddy.right(peakAng) | |
sz = 3.3 | |
def circle(rad): | |
ang = 15 # sides = 24 | |
opp = (180 - ang) / 2.0 | |
oppOpp = 90 - opp | |
mv = 2 * (rad * math.sin(math.radians(oppOpp))) | |
for i in range(24): | |
buddy.forward(mv) | |
buddy.left(ang) | |
def fractalPizza(x, y, lvl): | |
rad = math.pow(2, lvl) * sz | |
buddy.penup() | |
buddy.setpos(x, y) | |
buddy.pendown() | |
# pizza(rad, 6) | |
circle(rad) | |
if lvl > 0: | |
fractalPizza(x + rad, y, lvl - 1) | |
fractalPizza(x - rad, y, lvl - 1) | |
######################################################## | |
### DEMO USAGE. REPLACE WITH YOUR OWN CODE AS NEEDED ### | |
######################################################## | |
# GCTurtle can take any command that regular turtle can. | |
# You can just stick 'GC' in front of 'Turtle:' | |
# in your existing programs: | |
# buddy = Turtle() #<--change this | |
buddy = GCTurtle() # <--to this | |
buddy.drawPaper() | |
buddy.speed(0) | |
buddy.color("#448888") | |
buddy.pendown() | |
#pizza(120, 12) | |
fractalPizza(0,0,5) | |
# spits out the gcode | |
buddy.printGCode() | |
#buddy.showMachineMoves() | |
print("done") | |
#buddy.getscreen()._root.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment