turtle, util, dragon curve

This commit is contained in:
Michael Fogleman 2017-01-07 22:22:28 -05:00
parent d2ea5b6df2
commit c0525511d2
7 changed files with 220 additions and 15 deletions

View File

@ -1,3 +1,5 @@
from .device import Device from .device import Device
from .drawing import Drawing from .drawing import Drawing
from .planner import Planner from .planner import Planner
from .turtle import Turtle
from .util import draw

View File

@ -1,3 +1,5 @@
from __future__ import division
import time import time
from math import modf from math import modf
@ -9,12 +11,16 @@ from .planner import Planner
STEPS_PER_INCH = 2032 STEPS_PER_INCH = 2032
STEPS_PER_MM = 80 STEPS_PER_MM = 80
PEN_UP_POSITION = 60
PEN_UP_SPEED = 150
PEN_UP_DELAY = 100 PEN_UP_DELAY = 100
PEN_DOWN_POSITION = 40
PEN_DOWN_SPEED = 150
PEN_DOWN_DELAY = 100 PEN_DOWN_DELAY = 100
ACCELERATION = 6 ACCELERATION = 5
MAX_VELOCITY = 3 MAX_VELOCITY = 3
CORNER_FACTOR = 0.001 CORNER_FACTOR = 0.01
VID_PID = '04D8:FD92' VID_PID = '04D8:FD92'
@ -25,18 +31,44 @@ def find_port():
return None return None
class Device(object): class Device(object):
def __init__(self): def __init__(self, **kwargs):
port = find_port()
if port is None:
raise Exception('cannot find axidraw device')
self.serial = Serial(port, timeout=1)
self.steps_per_unit = STEPS_PER_INCH self.steps_per_unit = STEPS_PER_INCH
self.pen_up_position = PEN_UP_POSITION
self.pen_up_speed = PEN_UP_SPEED
self.pen_up_delay = PEN_UP_DELAY self.pen_up_delay = PEN_UP_DELAY
self.pen_down_position = PEN_DOWN_POSITION
self.pen_down_speed = PEN_DOWN_SPEED
self.pen_down_delay = PEN_DOWN_DELAY self.pen_down_delay = PEN_DOWN_DELAY
self.acceleration = ACCELERATION self.acceleration = ACCELERATION
self.max_velocity = MAX_VELOCITY self.max_velocity = MAX_VELOCITY
self.corner_factor = CORNER_FACTOR self.corner_factor = CORNER_FACTOR
for k, v in kwargs.items():
setattr(self, k, v)
port = find_port()
if port is None:
raise Exception('cannot find axidraw device')
self.serial = Serial(port, timeout=1)
self.configure()
def configure(self):
servo_min = 7500
servo_max = 28000
pen_up_position = self.pen_up_position / 100
pen_up_position = int(
servo_min + (servo_max - servo_min) * pen_up_position)
pen_down_position = self.pen_down_position / 100
pen_down_position = int(
servo_min + (servo_max - servo_min) * pen_down_position)
self.command('SC', 4, pen_up_position)
self.command('SC', 5, pen_down_position)
self.command('SC', 11, int(self.pen_up_speed * 5))
self.command('SC', 12, int(self.pen_down_speed * 5))
def close(self):
self.serial.close()
def make_planner(self): def make_planner(self):
return Planner( return Planner(
self.acceleration, self.max_velocity, self.corner_factor) self.acceleration, self.max_velocity, self.corner_factor)
@ -71,7 +103,7 @@ class Device(object):
def run_plan(self, plan): def run_plan(self, plan):
step_ms = 30 step_ms = 30
step_s = step_ms / 1000.0 step_s = step_ms / 1000
t = 0 t = 0
ex = 0 ex = 0
ey = 0 ey = 0
@ -91,12 +123,14 @@ class Device(object):
self.run_plan(plan) self.run_plan(plan)
def run_drawing(self, drawing): def run_drawing(self, drawing):
planner = self.make_planner()
self.pen_up() self.pen_up()
position = (0, 0) position = (0, 0)
for path in drawing.paths: for path in drawing.paths:
self.run_path([position, path[0]]) self.run_path([position, path[0]])
plan = planner.plan(path)
self.pen_down() self.pen_down()
self.run_path(path) self.run_plan(plan)
self.pen_up() self.pen_up()
position = path[-1] position = path[-1]
self.run_path([position, (0, 0)]) self.run_path([position, (0, 0)])

View File

@ -1,5 +1,9 @@
from __future__ import division
from math import sin, cos, radians from math import sin, cos, radians
from .paths import sort_paths
class Drawing(object): class Drawing(object):
def __init__(self, paths=None): def __init__(self, paths=None):
self.paths = paths or [] self.paths = paths or []
@ -29,8 +33,8 @@ class Drawing(object):
x1, y1, x2, y2 = self.bounds x1, y1, x2, y2 = self.bounds
return y2 - y1 return y2 - y1
# def sort_paths_greedy(self, reversable=True): def sort_paths(self, reversable=True):
# return Drawing(planner.sort_paths_greedy(self.paths, reversable)) return Drawing(sort_paths(self.paths, reversable))
# def join_paths(self, tolerance=0.05): # def join_paths(self, tolerance=0.05):
# return Drawing(util.join_paths(self.paths, tolerance)) # return Drawing(util.join_paths(self.paths, tolerance))
@ -70,18 +74,21 @@ class Drawing(object):
def origin(self): def origin(self):
return self.move(0, 0, 0, 0) return self.move(0, 0, 0, 0)
def center(self, width, height):
return self.move(width / 2, height / 2, 0.5, 0.5)
def rotate_to_fit(self, width, height, step=5): def rotate_to_fit(self, width, height, step=5):
for angle in range(0, 180, step): for angle in range(0, 180, step):
drawing = self.rotate(angle) drawing = self.rotate(angle)
if drawing.width <= width and drawing.height <= height: if drawing.width <= width and drawing.height <= height:
return drawing.origin() return drawing.center(width, height)
return None return None
def scale_to_fit(self, width, height, padding=0): def scale_to_fit(self, width, height, padding=0):
width -= padding * 2 width -= padding * 2
height -= padding * 2 height -= padding * 2
scale = min(width / self.width, height / self.height) scale = min(width / self.width, height / self.height)
return self.scale(scale, scale).origin() return self.scale(scale, scale).center(width, height)
def rotate_and_scale_to_fit(self, width, height, padding=0, step=5): def rotate_and_scale_to_fit(self, width, height, padding=0, step=5):
drawings = [] drawings = []
@ -92,4 +99,4 @@ class Drawing(object):
scale = min(width / drawing.width, height / drawing.height) scale = min(width / drawing.width, height / drawing.height)
drawings.append((scale, drawing)) drawings.append((scale, drawing))
scale, drawing = max(drawings) scale, drawing = max(drawings)
return drawing.scale(scale, scale).origin() return drawing.scale(scale, scale).center(width, height)

View File

@ -13,7 +13,7 @@ def sort_paths(paths, reversable=True):
points.append((x2, y2, path, True)) points.append((x2, y2, path, True))
index = Index(points) index = Index(points)
while index.size > 0: while index.size > 0:
x, y, path, reverse = index.search(result[-1][-1]) x, y, path, reverse = index.nearest(result[-1][-1])
x1, y1 = path[0] x1, y1 = path[0]
x2, y2 = path[-1] x2, y2 = path[-1]
index.remove((x1, y1, path, False)) index.remove((x1, y1, path, False))

134
axi/turtle.py Normal file
View File

@ -0,0 +1,134 @@
import math
from .drawing import Drawing
def to_degrees(x):
return math.degrees(x) % 360
class Turtle(object):
def __init__(self):
self.reset()
def reset(self):
self.x = 0
self.y = 0
self.h = 0
self.pen = True
self._path = [(self.x, self.y)]
self._paths = []
def clear(self):
self._path = [(self.x, self.y)]
self._paths = []
@property
def paths(self):
paths = list(self._paths)
if len(self._path) > 1:
paths.append(self._path)
return paths
@property
def drawing(self):
return Drawing(self.paths)
def pd(self):
self.pen = True
pendown = down = pd
def pu(self):
self.pen = False
if len(self._path) > 1:
self._paths.append(self._path)
self._path = [(self.x, self.y)]
penup = up = pu
def isdown(self):
return self.pen
def goto(self, x, y=None):
if y is None:
x, y = x
if self.pen:
self._path.append((x, y))
self.x = x
self.y = y
setpos = setposition = goto
def setx(self, x):
self.goto(x, self.y)
def sety(self, x):
self.goto(self.x, y)
def seth(self, heading):
self.h = heading
setheading = seth
def home(self):
self.goto(0, 0)
self.seth(0)
def fd(self, distance):
x = self.x + distance * math.cos(math.radians(self.h))
y = self.y + distance * math.sin(math.radians(self.h))
self.goto(x, y)
forward = fd
def bk(self, distance):
x = self.x - distance * math.cos(math.radians(self.h))
y = self.y - distance * math.sin(math.radians(self.h))
self.goto(x, y)
backward = back = bk
def rt(self, angle):
self.seth(self.h + angle)
right = rt
def lt(self, angle):
self.seth(self.h - angle)
left = lt
def circle(self, radius, extent=None, steps=None):
if extent is None:
extent = 360
if steps is None:
steps = int(round(abs(2 * math.pi * radius * extent / 360)))
steps = max(steps, 4)
cx = self.x + radius * math.cos(math.radians(self.h + 90))
cy = self.y + radius * math.sin(math.radians(self.h + 90))
a1 = to_degrees(math.atan2(self.y - cy, self.x - cx))
a2 = a1 + extent if radius >= 0 else a1 - extent
for i in range(steps):
p = i / float(steps - 1)
a = a1 + (a2 - a1) * p
x = cx + abs(radius) * math.cos(math.radians(a))
y = cy + abs(radius) * math.sin(math.radians(a))
self.goto(x, y)
if radius >= 0:
self.seth(self.h + extent)
else:
self.seth(self.h - extent)
def pos(self):
return (self.x, self.y)
position = pos
def towards(self, x, y=None):
if y is None:
x, y = x
return to_degrees(math.atan2(y - self.y, x - self.x))
def xcor(self):
return self.x
def ycor(self):
return self.y
def heading(self):
return self.h
def distance(self, x, y=None):
if y is None:
x, y = x
return math.hypot(x - self.x, y - self.y)

13
axi/util.py Normal file
View File

@ -0,0 +1,13 @@
from .device import Device
def reset():
d = Device()
d.disable_motors()
d.pen_up()
def draw(drawing):
# TODO: support drawing, list of paths, or single path
d = Device()
d.enable_motors()
d.run_drawing(drawing)
d.disable_motors()

15
examples/dragon_curve.py Normal file
View File

@ -0,0 +1,15 @@
import axi
def main(iteration):
turtle = axi.Turtle()
for i in range(1, 2 ** iteration):
turtle.forward(1)
if (((i & -i) << 1) & i) != 0:
turtle.circle(-1, 90, 36)
else:
turtle.circle(1, 90, 36)
drawing = turtle.drawing.rotate_and_scale_to_fit(11, 8.5, step=90)
axi.draw(drawing)
if __name__ == '__main__':
main(12)