From c0525511d28984be509e57951ba73060321f297a Mon Sep 17 00:00:00 2001 From: Michael Fogleman Date: Sat, 7 Jan 2017 22:22:28 -0500 Subject: [PATCH] turtle, util, dragon curve --- axi/__init__.py | 2 + axi/device.py | 52 ++++++++++++--- axi/drawing.py | 17 +++-- axi/paths.py | 2 +- axi/turtle.py | 134 +++++++++++++++++++++++++++++++++++++++ axi/util.py | 13 ++++ examples/dragon_curve.py | 15 +++++ 7 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 axi/turtle.py create mode 100644 axi/util.py create mode 100644 examples/dragon_curve.py diff --git a/axi/__init__.py b/axi/__init__.py index 4c86400..bee7761 100644 --- a/axi/__init__.py +++ b/axi/__init__.py @@ -1,3 +1,5 @@ from .device import Device from .drawing import Drawing from .planner import Planner +from .turtle import Turtle +from .util import draw diff --git a/axi/device.py b/axi/device.py index c0e5b6a..7b0025d 100644 --- a/axi/device.py +++ b/axi/device.py @@ -1,3 +1,5 @@ +from __future__ import division + import time from math import modf @@ -9,12 +11,16 @@ from .planner import Planner STEPS_PER_INCH = 2032 STEPS_PER_MM = 80 +PEN_UP_POSITION = 60 +PEN_UP_SPEED = 150 PEN_UP_DELAY = 100 +PEN_DOWN_POSITION = 40 +PEN_DOWN_SPEED = 150 PEN_DOWN_DELAY = 100 -ACCELERATION = 6 +ACCELERATION = 5 MAX_VELOCITY = 3 -CORNER_FACTOR = 0.001 +CORNER_FACTOR = 0.01 VID_PID = '04D8:FD92' @@ -25,18 +31,44 @@ def find_port(): return None class Device(object): - def __init__(self): - port = find_port() - if port is None: - raise Exception('cannot find axidraw device') - self.serial = Serial(port, timeout=1) + def __init__(self, **kwargs): 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_down_position = PEN_DOWN_POSITION + self.pen_down_speed = PEN_DOWN_SPEED self.pen_down_delay = PEN_DOWN_DELAY self.acceleration = ACCELERATION self.max_velocity = MAX_VELOCITY 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): return Planner( self.acceleration, self.max_velocity, self.corner_factor) @@ -71,7 +103,7 @@ class Device(object): def run_plan(self, plan): step_ms = 30 - step_s = step_ms / 1000.0 + step_s = step_ms / 1000 t = 0 ex = 0 ey = 0 @@ -91,12 +123,14 @@ class Device(object): self.run_plan(plan) def run_drawing(self, drawing): + planner = self.make_planner() self.pen_up() position = (0, 0) for path in drawing.paths: self.run_path([position, path[0]]) + plan = planner.plan(path) self.pen_down() - self.run_path(path) + self.run_plan(plan) self.pen_up() position = path[-1] self.run_path([position, (0, 0)]) diff --git a/axi/drawing.py b/axi/drawing.py index 45e4a93..6c91541 100644 --- a/axi/drawing.py +++ b/axi/drawing.py @@ -1,5 +1,9 @@ +from __future__ import division + from math import sin, cos, radians +from .paths import sort_paths + class Drawing(object): def __init__(self, paths=None): self.paths = paths or [] @@ -29,8 +33,8 @@ class Drawing(object): x1, y1, x2, y2 = self.bounds return y2 - y1 - # def sort_paths_greedy(self, reversable=True): - # return Drawing(planner.sort_paths_greedy(self.paths, reversable)) + def sort_paths(self, reversable=True): + return Drawing(sort_paths(self.paths, reversable)) # def join_paths(self, tolerance=0.05): # return Drawing(util.join_paths(self.paths, tolerance)) @@ -70,18 +74,21 @@ class Drawing(object): def origin(self): 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): for angle in range(0, 180, step): drawing = self.rotate(angle) if drawing.width <= width and drawing.height <= height: - return drawing.origin() + return drawing.center(width, height) return None def scale_to_fit(self, width, height, padding=0): width -= padding * 2 height -= padding * 2 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): drawings = [] @@ -92,4 +99,4 @@ class Drawing(object): scale = min(width / drawing.width, height / drawing.height) drawings.append((scale, drawing)) scale, drawing = max(drawings) - return drawing.scale(scale, scale).origin() + return drawing.scale(scale, scale).center(width, height) diff --git a/axi/paths.py b/axi/paths.py index b27edd1..0930b9d 100644 --- a/axi/paths.py +++ b/axi/paths.py @@ -13,7 +13,7 @@ def sort_paths(paths, reversable=True): points.append((x2, y2, path, True)) index = Index(points) 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] x2, y2 = path[-1] index.remove((x1, y1, path, False)) diff --git a/axi/turtle.py b/axi/turtle.py new file mode 100644 index 0000000..f458ce8 --- /dev/null +++ b/axi/turtle.py @@ -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) diff --git a/axi/util.py b/axi/util.py new file mode 100644 index 0000000..42a68f8 --- /dev/null +++ b/axi/util.py @@ -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() diff --git a/examples/dragon_curve.py b/examples/dragon_curve.py new file mode 100644 index 0000000..2e82f07 --- /dev/null +++ b/examples/dragon_curve.py @@ -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)