Skip to content

Instantly share code, notes, and snippets.

@melsov
Last active March 15, 2018 16:44
Show Gist options
  • Save melsov/3f52b7fc62be16bdc3fbe64b1e10775e to your computer and use it in GitHub Desktop.
Save melsov/3f52b7fc62be16bdc3fbe64b1e10775e to your computer and use it in GitHub Desktop.
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