from shapely import geometry, ops import cPickle as pickle import math import planner import util def shapely_paths(shape): if hasattr(shape, 'coords'): return [list(shape.coords)] elif hasattr(shape, 'exterior'): paths = [] paths.append(list(shape.exterior.coords)) for interior in shape.interiors: paths.append(list(interior.coords)) return paths elif hasattr(shape, 'geoms'): paths = [] for child in shape.geoms: paths.extend(shapely_paths(child)) return paths else: raise Exception class Drawing(object): def __init__(self, paths=None): self.paths = paths or [] self._bounds = None @staticmethod def from_shapely(shape): return Drawing(shapely_paths(shape)) def to_shapely(self): return geometry.MultiLineString(self.paths) @staticmethod def load(path): with open(path, 'rb') as fp: return Drawing(pickle.load(fp)) def save(self, path): with open(path, 'wb') as fp: pickle.dump(self.paths, fp, -1) @property def bounds(self): if not self._bounds: points = [(x, y) for path in self.paths for x, y in path] if points: x1 = min(x for x, y in points) x2 = max(x for x, y in points) y1 = min(y for x, y in points) y2 = max(y for x, y in points) else: x1 = x2 = y1 = y2 = 0 self._bounds = (x1, y1, x2, y2) return self._bounds @property def width(self): x1, y1, x2, y2 = self.bounds return x2 - x1 @property def height(self): 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, iterations=100000, reversable=True): return Drawing(planner.sort_paths(self.paths, iterations, reversable)) def join_paths(self, tolerance=0.05): return Drawing(util.join_paths(self.paths, tolerance)) def remove_duplicates(self): return Drawing(util.remove_duplicates(self.paths)) def simplify_paths(self, tolerance=0.05): return Drawing(util.simplify_paths(self.paths, tolerance)) def crop(self, x1, y1, x2, y2): box = geometry.Polygon([ (x1, y1), (x2, y1), (x2, y2), (x1, y2), (x1, y1), ]) return Drawing.from_shapely(box.intersection(self.to_shapely())) def linemerge(self): lines = ops.linemerge([geometry.LineString(x) for x in self.paths]) return Drawing.from_shapely(lines) def transform(self, func): return Drawing([[func(x, y) for x, y in path] for path in self.paths]) def translate(self, dx, dy): def func(x, y): return (x + dx, y + dy) return self.transform(func) def scale(self, sx, sy): def func(x, y): return (x * sx, y * sy) return self.transform(func) def rotate(self, angle): c = math.cos(math.radians(angle)) s = math.sin(math.radians(angle)) def func(x, y): return (x * c - y * s, y * c + x * s) return self.transform(func) def move(self, x, y, ax, ay): x1, y1, x2, y2 = self.bounds dx = x1 + (x2 - x1) * ax - x dy = y1 + (y2 - y1) * ay - y return self.translate(-dx, -dy) def origin(self): return self.move(0, 0, 0, 0) def rotate_to_fit(self, width, height, step=5): for a in range(0, 180, step): g = self.rotate(a) if g.width <= width and g.height <= height: return g.origin() return None def scale_to_fit(self, width, height, padding=0): width -= padding * 2 height -= padding * 2 s = min(width / self.width, height / self.height) return self.scale(s, s).origin() def rotate_and_scale_to_fit(self, width, height, padding=0, step=5): gs = [] width -= padding * 2 height -= padding * 2 for a in range(0, 180, step): g = self.rotate(a) s = min(width / g.width, height / g.height) gs.append((s, a, g)) s, a, g = max(gs) return g.scale(s, s).origin() def render(self, scale=96/25.4, margin=10, line_width=0.5): import cairo x1, y1, x2, y2 = self.bounds width = int(scale * self.width + margin * 2) height = int(scale * self.height + margin * 2) surface = cairo.ImageSurface(cairo.FORMAT_RGB24, width, height) dc = cairo.Context(surface) dc.set_line_cap(cairo.LINE_CAP_ROUND) dc.translate(margin, height - margin) dc.scale(scale, -scale) dc.translate(-x1, -y1) dc.set_line_width(line_width) dc.set_source_rgb(1, 1, 1) dc.paint() # dc.arc(0, 0, 3.0 / scale, 0, 2 * math.pi) # dc.set_source_rgb(1, 0, 0) # dc.fill() dc.set_source_rgb(0, 0, 0) for path in self.paths: dc.move_to(*path[0]) for x, y in path: dc.line_to(x, y) dc.stroke() return surface