## gcodesaver.py is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by the ## Free Software Foundation; either version 2 of the License, or (at your ## option) any later version. gcodesaver.py is distributed in the hope ## that it will be useful, but WITHOUT ANY WARRANTY; without even the implied ## warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See ## the GNU General Public License for more details. You should have ## received a copy of the GNU General Public License along with gcodesaver.py; ## if not, write to the Free Software Foundation, Inc., 59 Temple Place, ## Suite 330, Boston, MA 02111-1307 USA ## ## image-to-gcode is Copyright (C) 2005 Chris Radek ## chris@timeguy.com ## gcodesaver.py is Copyright (C) 2006 Jeff Epler ## jepler@unpy.net ###Sketch Config #type = Export #tk_file_type = ('RS274NGC Document', '.ngc') #format_name = 'gcode' #extensions = '.ngc' ###End def dist_lseg(l1, l2, p): x0, y0, z0 = l1 xa, ya, za = l2 xi, yi, zi = p dx = xa-x0 dy = ya-y0 dz = za-z0 d2 = dx*dx + dy*dy + dz*dz if d2 == 0: return 0 t = (dx * (xi-x0) + dy * (yi-y0) + dz * (zi-z0)) / d2 if t < 0: t = 0 if t > 1: t = 1 dist2 = (xi - x0 - t * dx) ** 2 + (yi - y0 - t * dy) ** 2 + (zi - z0 - t * dz) ** 2 return dist2 ** .5 def douglas(st, tolerance=.01, first=True): if len(st) == 1: yield st[0] return l1 = st[0] l2 = st[-1] worst_dist = 0 worst = 0 for i, p in enumerate(st): if p is l1 or p is l2: continue dist = dist_lseg(l1, l2, p) if dist > worst_dist: worst = i worst_dist = dist if first: yield st[0] if worst_dist > tolerance: for i in douglas(st[:worst+1], tolerance, False): yield i yield st[worst] for i in douglas(st[worst:], tolerance, False): yield i if first: yield st[-1] class gcode: def __init__(self, homeheight = 1.5, safetyheight = 0.04, tolerance=0.001): self.lastx = self.lasty = self.lastz = self.lasta = None self.lastgcode = self.lastfeed = None self.homeheight = homeheight self.safetyheight = self.lastz = safetyheight self.tolerance = tolerance self.cuts = [] self.time = 0 def begin(self): self.write("G0 Z%.4f\n" % (self.safetyheight) + \ "G17 G20 G40 G49\n" + "G54 G80 G90 G94\n" + \ "S1000 M3\n" + "G04 P3\n") def comment(self, s): s = s.replace("(", "").replace(")", "").replace("\n", "") self.write("(%s)\n" % s) def flush(self): if not self.cuts: return for x, y, z in douglas(self.cuts, self.tolerance): self.move_common(x, y, z, gcode="G1") self.cuts = [] def end(self): self.flush() self.safety() self.write("M2\n") def exactpath(self): self.write("G61\n") def continuous(self): self.write("G64\n") def rapid(self, x=None, y=None, z=None, a=None): self.flush() self.move_common(x, y, z, a, "G0\n") def move_common(self, x=None, y=None, z=None, a=None, gcode="G0"): gcodestring = xstring = ystring = zstring = astring = "" if x == None: x = self.lastx if y == None: y = self.lasty if z == None: z = self.lastz if a == None: a = self.lasta if gcode != self.lastgcode: gcodestring = gcode self.lastgcode = gcode if x != self.lastx: xstring = " X%.4f" % (x) self.lastx = x if y != self.lasty: ystring = " Y%.4f" % (y) self.lasty = y if z != self.lastz: zstring = " Z%.4f" % (z) self.lastz = z if a != self.lasta: astring = " A%.4f" % (a) self.lasta = a cmd = "".join([gcodestring, xstring, ystring, zstring, astring]) if cmd: self.write(cmd + "\n") def set_feed(self, feed): self.write("F%.4f\n" % feed) def cut(self, x=None, y=None, z=None): if self.cuts: lastx, lasty, lastz = self.cuts[-1] else: lastx, lasty, lastz = self.lastx, self.lasty, self.lastz if x is None: x = lastx if y == None: y = lasty if z == None: z = lastz self.cuts.append([x,y,z]) def home(self): self.flush() self.rapid(z=self.homeheight) def safety(self): self.flush() self.rapid(z=self.safetyheight) def subdivide(p0, p1, p2, p3, t = 0.5): t2 = 1 - t r = t2 * p1 + t * p2 q1 = t2 * p0 + t * p1 q2 = t2 * q1 + t * r q5 = t2 * p2 + t * p3 q4 = t2 * r + t * q5 q3 = t2 * q2 + t * q4 return q1, q2, q3, q4, q5 from Sketch import Bezier class GcodeSaver(gcode): def __init__(self, file, filename, document, options): gcode.__init__(self) self.file = file self.filename = filename self.document = document self.__dict__.update(options) self.first_in_path = True self.feedrate = 10 self.depth = 0 def write(self, what): self.file.write(what) def try_convert(self, v): try: return int(v) except ValueError: return float(v) except: return v def update_metadata(self, layer): words = layer.name.split() for w in words: if "=" in w: k, v = w.split("=", 1) setattr(self, k, self.try_convert(v)) def Save(self): bbox = self.document.BoundingRect() self.comment("exported from skencil by gcode.py") self.comment("bounding rectangle: " + repr(bbox)) self.begin() for layer in reversed(self.document.Layers()): if not layer.is_SpecialLayer and layer.Printable(): self.update_metadata(layer) self.save_objects(layer.GetObjects()) self.end() def path_end(self): self.safety() self.first_in_path = True def path_point(self, p): if self.first_in_path: self.first_in_path = False self.first_point = p self.rapid(p.x/72., p.y/72.) self.set_feed(self.feedrate) self.cut(z=self.depth) else: self.cut(p.x/72., p.y/72.) def path_close(self): self.path_point(self.first_point) def save_object(self, o): paths = o.Paths() props = o.Properties() bbox = o.bounding_rect self.comment("exporting object %r" % o) self.comment("bounding rectangle: " + repr(bbox)) for path in paths: for i in range(path.len): type, control, p, cont = path.Segment(i) if type == Bezier: p1, p2 = control for j in range(100): q = subdivide(p0, p1, p2, p, j / 100.)[2] self.path_point(q) if isinstance(p, tuple): for q in p: self.path_point(p) p0 = p if path.closed: self.path_close() self.path_end() def save_objects(self, objects): for o in objects: if o.is_Compound: save_objects(o.GetObjects()) elif o.is_Bezier or o.is_Rectangle or o.is_Ellipse: self.save_object(o) else: self.comment("Skipping object %r" % o) def close(self): self.home() self.end() def save(document, file, filename, options = {}): saver = GcodeSaver(file, filename, document, options) saver.Save() saver.close()