| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091 |
- from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
- from matplotlib.figure import Figure
- import re
- # See: http://toblerity.org/shapely/manual.html
- from shapely.geometry import Polygon, LineString, Point, LinearRing
- from shapely.geometry import MultiPoint, MultiPolygon
- from shapely.geometry import box as shply_box
- from shapely.ops import cascaded_union
- import shapely.affinity as affinity
- # Used for solid polygons in Matplotlib
- from descartes.patch import PolygonPatch
- class Geometry:
- def __init__(self):
- # Units (in or mm)
- self.units = 'in'
-
- # Final geometry: MultiPolygon
- self.solid_geometry = None
-
- def isolation_geometry(self, offset):
- """
- Creates contours around geometry at a given
- offset distance.
- """
- return self.solid_geometry.buffer(offset)
-
- def bounds(self):
- """
- Returns coordinates of rectangular bounds
- of geometry: (xmin, ymin, xmax, ymax).
- """
- if self.solid_geometry is None:
- print "Warning: solid_geometry not computed yet."
- return (0, 0, 0, 0)
-
- if type(self.solid_geometry) == list:
- return cascaded_union(self.solid_geometry).bounds
- else:
- return self.solid_geometry.bounds
-
- def size(self):
- """
- Returns (width, height) of rectangular
- bounds of geometry.
- """
- if self.solid_geometry is None:
- print "Warning: solid_geometry not computed yet."
- return 0
- bounds = self.bounds()
- return (bounds[2]-bounds[0], bounds[3]-bounds[1])
-
- def get_empty_area(self, boundary=None):
- """
- Returns the complement of self.solid_geometry within
- the given boundary polygon. If not specified, it defaults to
- the rectangular bounding box of self.solid_geometry.
- """
- if boundary is None:
- boundary = self.solid_geometry.envelope
- return boundary.difference(self.solid_geometry)
-
- def clear_polygon(self, polygon, tooldia, overlap=0.15):
- """
- Creates geometry inside a polygon for a tool to cover
- the whole area.
- """
- poly_cuts = [polygon.buffer(-tooldia/2.0)]
- while True:
- polygon = poly_cuts[-1].buffer(-tooldia*(1-overlap))
- if polygon.area > 0:
- poly_cuts.append(polygon)
- else:
- break
- return poly_cuts
- def scale(self, factor):
- """
- Scales all of the object's geometry by a given factor. Override
- this method.
- :param factor: Number by which to scale.
- :type factor: float
- :return: None
- :rtype: None
- """
- return
- def convert_units(self, units):
- """
- Converts the units of the object to ``units`` by scaling all
- the geometry appropriately.
- :param units: "IN" or "MM"
- :type units: str
- :return: Scaling factor resulting from unit change.
- :rtype: float
- """
- if units.upper() == self.units.upper():
- return 1.0
- if units.upper() == "MM":
- factor = 25.4
- elif units.upper() == "IN":
- factor = 1/25.4
- else:
- print "Unsupported units:", units
- return 1.0
- self.units = units
- self.scale(factor)
- return factor
-
- class Gerber (Geometry):
- """
- **ATTRIBUTES**
- * ``apertures`` (dict): The keys are names/identifiers of each aperture.
- The values are dictionaries key/value pairs which describe the aperture. The
- type key is always present and the rest depend on the key:
- +-----------+-----------------------------------+
- | Key | Value |
- +===========+===================================+
- | type | (str) "C", "R", or "O" |
- +-----------+-----------------------------------+
- | others | Depend on ``type`` |
- +-----------+-----------------------------------+
- * ``paths`` (list): A path is described by a line an aperture that follows that
- line. Each paths[i] is a dictionary:
- +------------+------------------------------------------------+
- | Key | Value |
- +============+================================================+
- | linestring | (Shapely.LineString) The actual path. |
- +------------+------------------------------------------------+
- | aperture | (str) The key for an aperture in apertures. |
- +------------+------------------------------------------------+
- * ``flashes`` (list): Flashes are single-point strokes of an aperture. Each
- is a dictionary:
- +------------+------------------------------------------------+
- | Key | Value |
- +============+================================================+
- | loc | (list) [x (float), y (float)] coordinates. |
- +------------+------------------------------------------------+
- | aperture | (str) The key for an aperture in apertures. |
- +------------+------------------------------------------------+
- * ``regions`` (list): Are surfaces defined by a polygon (Shapely.Polygon),
- which have an exterior and zero or more interiors. An aperture is also
- associated with a region. Each is a dictionary:
- +------------+-----------------------------------------------------+
- | Key | Value |
- +============+=====================================================+
- | polygon | (Shapely.Polygon) The polygon defining the region. |
- +------------+-----------------------------------------------------+
- | aperture | (str) The key for an aperture in apertures. |
- +------------+-----------------------------------------------------+
- * ``flash_geometry`` (list): List of (Shapely) geometric object resulting
- from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``.
- * ``buffered_paths`` (list): List of (Shapely) polygons resulting from
- *buffering* (or thickening) the ``paths`` with the aperture. These are
- generated from ``paths`` in ``buffer_paths()``.
- """
- def __init__(self):
- # Initialize parent
- Geometry.__init__(self)
-
- # Number format
- self.digits = 3
- """Number of integer digits in Gerber numbers. Used during parsing."""
- self.fraction = 4
- """Number of fraction digits in Gerber numbers. Used during parsing."""
-
- ## Gerber elements ##
- # Apertures {'id':{'type':chr,
- # ['size':float], ['width':float],
- # ['height':float]}, ...}
- self.apertures = {}
-
- # Paths [{'linestring':LineString, 'aperture':str}]
- self.paths = []
-
- # Buffered Paths [Polygon]
- # Paths transformed into Polygons by
- # offsetting the aperture size/2
- self.buffered_paths = []
-
- # Polygon regions [{'polygon':Polygon, 'aperture':str}]
- self.regions = []
-
- # Flashes [{'loc':[float,float], 'aperture':str}]
- self.flashes = []
-
- # Geometry from flashes
- self.flash_geometry = []
- def scale(self, factor):
- """
- Scales the objects' geometry on the XY plane by a given factor.
- These are:
- * ``apertures``
- * ``paths``
- * ``regions``
- * ``flashes``
- Then ``buffered_paths``, ``flash_geometry`` and ``solid_geometry``
- are re-created with ``self.create_geometry()``.
- :param factor: Number by which to scale.
- :type factor: float
- :rtype : None
- """
- # Apertures
- print "Scaling apertures..."
- for apid in self.apertures:
- for param in self.apertures[apid]:
- if param != "type": # All others are dimensions.
- print "Tool:", apid, "Parameter:", param
- self.apertures[apid][param] *= factor
- # Paths
- print "Scaling paths..."
- for path in self.paths:
- path['linestring'] = affinity.scale(path['linestring'],
- factor, factor, origin=(0, 0))
- # Flashes
- print "Scaling flashes..."
- for fl in self.flashes:
- # TODO: Shouldn't 'loc' be a numpy.array()?
- fl['loc'][0] *= factor
- fl['loc'][1] *= factor
- # Regions
- print "Scaling regions..."
- for reg in self.regions:
- reg['polygon'] = affinity.scale(reg['polygon'], factor, factor,
- origin=(0, 0))
- # Now buffered_paths, flash_geometry and solid_geometry
- self.create_geometry()
- def fix_regions(self):
- """
- Overwrites the region polygons with fixed
- versions if found to be invalid (according to Shapely).
- """
- for region in self.regions:
- if not region['polygon'].is_valid:
- region['polygon'] = region['polygon'].buffer(0)
-
- def buffer_paths(self):
- self.buffered_paths = []
- for path in self.paths:
- width = self.apertures[path["aperture"]]["size"]
- self.buffered_paths.append(path["linestring"].buffer(width/2))
-
- def aperture_parse(self, gline):
- """
- Parse gerber aperture definition into dictionary of apertures.
- The following kinds and their attributes are supported:
- * *Circular (C)*: size (float)
- * *Rectangle (R)*: width (float), height (float)
- * *Obround (O)*: width (float), height (float). NOTE: This can
- be parsed, but it is not supported further yet.
- """
- indexstar = gline.find("*")
- indexc = gline.find("C,")
- if indexc != -1: # Circle, example: %ADD11C,0.1*%
- apid = gline[4:indexc]
- self.apertures[apid] = {"type": "C",
- "size": float(gline[indexc+2:indexstar])}
- return apid
- indexr = gline.find("R,")
- if indexr != -1: # Rectangle, example: %ADD15R,0.05X0.12*%
- apid = gline[4:indexr]
- indexx = gline.find("X")
- self.apertures[apid] = {"type": "R",
- "width": float(gline[indexr+2:indexx]),
- "height": float(gline[indexx+1:indexstar])}
- return apid
- indexo = gline.find("O,")
- if indexo != -1: # Obround
- apid = gline[4:indexo]
- indexx = gline.find("X")
- self.apertures[apid] = {"type": "O",
- "width": float(gline[indexo+2:indexx]),
- "height": float(gline[indexx+1:indexstar])}
- return apid
- print "WARNING: Aperture not implemented:", gline
- return None
-
- def parse_file(self, filename):
- """
- Calls Gerber.parse_lines() with array of lines
- read from the given file.
- """
- gfile = open(filename, 'r')
- gstr = gfile.readlines()
- gfile.close()
- self.parse_lines(gstr)
-
- def parse_lines(self, glines):
- """
- Main Gerber parser.
- """
- # Mode (IN/MM)
- mode_re = re.compile(r'^%MO(IN|MM)\*%$')
- path = [] # Coordinates of the current path
- last_path_aperture = None
- current_aperture = None
-
- for gline in glines:
-
- if gline.find("D01*") != -1: # pen down
- path.append(coord(gline, self.digits, self.fraction))
- last_path_aperture = current_aperture
- continue
-
- if gline.find("D02*") != -1: # pen up
- if len(path) > 1:
- # Path completed, create shapely LineString
- self.paths.append({"linestring": LineString(path),
- "aperture": last_path_aperture})
- path = [coord(gline, self.digits, self.fraction)]
- continue
-
- indexd3 = gline.find("D03*")
- if indexd3 > 0: # Flash
- self.flashes.append({"loc": coord(gline, self.digits, self.fraction),
- "aperture": current_aperture})
- continue
- if indexd3 == 0: # Flash?
- print "WARNING: Uninplemented flash style:", gline
- continue
-
- if gline.find("G37*") != -1: # end region
- # Only one path defines region?
- self.regions.append({"polygon": Polygon(path),
- "aperture": last_path_aperture})
- path = []
- continue
-
- if gline.find("%ADD") != -1: # aperture definition
- self.aperture_parse(gline) # adds element to apertures
- continue
-
- indexstar = gline.find("*")
- if gline.find("D") == 0: # Aperture change
- current_aperture = gline[1:indexstar]
- continue
- if gline.find("G54D") == 0: # Aperture change (deprecated)
- current_aperture = gline[4:indexstar]
- continue
-
- if gline.find("%FS") != -1: # Format statement
- indexx = gline.find("X")
- self.digits = int(gline[indexx + 1])
- self.fraction = int(gline[indexx + 2])
- continue
- # Mode (IN/MM)
- match = mode_re.search(gline)
- if match:
- self.units = match.group(1)
- continue
- print "WARNING: Line ignored:", gline
-
- if len(path) > 1:
- # EOF, create shapely LineString if something still in path
- self.paths.append({"linestring": LineString(path),
- "aperture": last_path_aperture})
-
- def do_flashes(self):
- """
- Creates geometry for Gerber flashes (aperture on a single point).
- """
- self.flash_geometry = []
- for flash in self.flashes:
- aperture = self.apertures[flash['aperture']]
- if aperture['type'] == 'C': # Circles
- circle = Point(flash['loc']).buffer(aperture['size']/2)
- self.flash_geometry.append(circle)
- continue
- if aperture['type'] == 'R': # Rectangles
- loc = flash['loc']
- width = aperture['width']
- height = aperture['height']
- minx = loc[0] - width/2
- maxx = loc[0] + width/2
- miny = loc[1] - height/2
- maxy = loc[1] + height/2
- rectangle = shply_box(minx, miny, maxx, maxy)
- self.flash_geometry.append(rectangle)
- continue
- #TODO: Add support for type='O'
- print "WARNING: Aperture type %s not implemented" % (aperture['type'])
-
- def create_geometry(self):
- """
- Geometry from a Gerber file is made up entirely of polygons.
- Every stroke (linear or circular) has an aperture which gives
- it thickness. Additionally, aperture strokes have non-zero area,
- and regions naturally do as well.
- :rtype : None
- @return: None
- """
- # if len(self.buffered_paths) == 0:
- # self.buffer_paths()
- self.buffer_paths()
- self.fix_regions()
- self.do_flashes()
- self.solid_geometry = cascaded_union(
- self.buffered_paths +
- [poly['polygon'] for poly in self.regions] +
- self.flash_geometry)
- class Excellon(Geometry):
- """
- *ATTRIBUTES*
- * ``tools`` (dict): The key is the tool name and the value is
- the size (diameter).
- * ``drills`` (list): Each is a dictionary:
- ================ ====================================
- Key Value
- ================ ====================================
- point (Shapely.Point) Where to drill
- tool (str) A key in ``tools``
- ================ ====================================
- """
- def __init__(self):
- Geometry.__init__(self)
-
- self.tools = {}
-
- self.drills = []
- # Trailing "T" or leading "L"
- self.zeros = ""
-
- def parse_file(self, filename):
- efile = open(filename, 'r')
- estr = efile.readlines()
- efile.close()
- self.parse_lines(estr)
-
- def parse_lines(self, elines):
- """
- Main Excellon parser.
- """
- units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?$')
- current_tool = ""
-
- for eline in elines:
-
- ## Tool definitions ##
- # TODO: Verify all this
- indexT = eline.find("T")
- indexC = eline.find("C")
- indexF = eline.find("F")
- # Type 1
- if indexT != -1 and indexC > indexT and indexF > indexC:
- tool = eline[1:indexC]
- spec = eline[indexC+1:indexF]
- self.tools[tool] = float(spec)
- continue
- # Type 2
- # TODO: Is this inches?
- #indexsp = eline.find(" ")
- #indexin = eline.find("in")
- #if indexT != -1 and indexsp > indexT and indexin > indexsp:
- # tool = eline[1:indexsp]
- # spec = eline[indexsp+1:indexin]
- # self.tools[tool] = spec
- # continue
- # Type 3
- if indexT != -1 and indexC > indexT:
- tool = eline[1:indexC]
- spec = eline[indexC+1:-1]
- self.tools[tool] = float(spec)
- continue
-
- ## Tool change
- if indexT == 0:
- current_tool = eline[1:-1]
- continue
-
- ## Drill
- indexx = eline.find("X")
- indexy = eline.find("Y")
- if indexx != -1 and indexy != -1:
- x = float(int(eline[indexx+1:indexy])/10000.0)
- y = float(int(eline[indexy+1:-1])/10000.0)
- self.drills.append({'point': Point((x, y)), 'tool': current_tool})
- continue
- # Units and number format
- match = units_re.match(eline)
- if match:
- self.zeros = match.group(2) # "T" or "L"
- self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
- print "WARNING: Line ignored:", eline
-
- def create_geometry(self):
- self.solid_geometry = []
- sizes = {}
- for tool in self.tools:
- sizes[tool] = float(self.tools[tool])
- for drill in self.drills:
- poly = Point(drill['point']).buffer(sizes[drill['tool']]/2.0)
- self.solid_geometry.append(poly)
- self.solid_geometry = cascaded_union(self.solid_geometry)
- def scale(self, factor):
- """
- Scales geometry on the XY plane in the object by a given factor.
- Tool sizes, feedrates an Z-plane dimensions are untouched.
- :param factor: Number by which to scale the object.
- :type factor: float
- :return: None
- :rtype: NOne
- """
- # Drills
- for drill in self.drills:
- drill.point = affinity.scale(drill.point, factor, factor, origin=(0, 0))
- def convert_units(self, units):
- factor = Geometry.convert_units(self, units)
- # Tools
- for tname in self.tools:
- self.tools[tname] *= factor
- return factor
- class CNCjob(Geometry):
- """
- Represents work to be done by a CNC machine.
- *ATTRIBUTES*
- * ``gcode_parsed`` (list): Each is a dictionary:
- ===================== =========================================
- Key Value
- ===================== =========================================
- geom (Shapely.LineString) Tool path (XY plane)
- kind (string) "AB", A is "T" (travel) or
- "C" (cut). B is "F" (fast) or "S" (slow).
- ===================== =========================================
- """
- def __init__(self, units="in", kind="generic", z_move=0.1,
- feedrate=3.0, z_cut=-0.002, tooldia=0.0):
- Geometry.__init__(self)
- self.kind = kind
- self.units = units
- self.z_cut = z_cut
- self.z_move = z_move
- self.feedrate = feedrate
- self.tooldia = tooldia
- self.unitcode = {"IN": "G20", "MM": "G21"}
- self.pausecode = "G04 P1"
- self.feedminutecode = "G94"
- self.absolutecode = "G90"
- self.gcode = ""
- self.input_geometry_bounds = None
- self.gcode_parsed = None
- self.steps_per_circ = 20 # Used when parsing G-code arcs
-
- def generate_from_excellon(self, exobj):
- """
- Generates G-code for drilling from Excellon object.
- self.gcode becomes a list, each element is a
- different job for each tool in the excellon code.
- """
- self.kind = "drill"
- self.gcode = []
-
- t = "G00 X%.4fY%.4f\n"
- down = "G01 Z%.4f\n" % self.z_cut
- up = "G01 Z%.4f\n" % self.z_move
- for tool in exobj.tools:
-
- points = []
-
- for drill in exobj.drill:
- if drill['tool'] == tool:
- points.append(drill['point'])
-
- gcode = self.unitcode[self.units.upper()] + "\n"
- gcode += self.absolutecode + "\n"
- gcode += self.feedminutecode + "\n"
- gcode += "F%.2f\n" % self.feedrate
- gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
- gcode += "M03\n" # Spindle start
- gcode += self.pausecode + "\n"
-
- for point in points:
- gcode += t % point
- gcode += down + up
-
- gcode += t % (0, 0)
- gcode += "M05\n" # Spindle stop
-
- self.gcode.append(gcode)
- def generate_from_excellon_by_tool(self, exobj, tools="all"):
- """
- Creates gcode for this object from an Excellon object
- for the specified tools.
- @param exobj: Excellon object to process
- @type exobj: Excellon
- @param tools: Comma separated tool names
- @type: tools: str
- @return: None
- """
- print "Creating CNC Job from Excellon..."
- if tools == "all":
- tools = [tool for tool in exobj.tools]
- else:
- tools = [x.strip() for x in tools.split(",")]
- tools = filter(lambda y: y in exobj.tools, tools)
- print "Tools are:", tools
- points = []
- for drill in exobj.drills:
- if drill['tool'] in tools:
- points.append(drill['point'])
- print "Found %d drills." % len(points)
- #self.kind = "drill"
- self.gcode = []
- t = "G00 X%.4fY%.4f\n"
- down = "G01 Z%.4f\n" % self.z_cut
- up = "G01 Z%.4f\n" % self.z_move
- gcode = self.unitcode[self.units.upper()] + "\n"
- gcode += self.absolutecode + "\n"
- gcode += self.feedminutecode + "\n"
- gcode += "F%.2f\n" % self.feedrate
- gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
- gcode += "M03\n" # Spindle start
- gcode += self.pausecode + "\n"
- for point in points:
- x, y = point.coords.xy
- gcode += t % (x[0], y[0])
- gcode += down + up
- gcode += t % (0, 0)
- gcode += "M05\n" # Spindle stop
- self.gcode = gcode
- def generate_from_geometry(self, geometry, append=True, tooldia=None):
- """
- Generates G-Code from a Geometry object.
- """
- if tooldia is not None:
- self.tooldia = tooldia
-
- self.input_geometry_bounds = geometry.bounds()
-
- if not append:
- self.gcode = ""
- self.gcode = self.unitcode[self.units.upper()] + "\n"
- self.gcode += self.absolutecode + "\n"
- self.gcode += self.feedminutecode + "\n"
- self.gcode += "F%.2f\n" % self.feedrate
- self.gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
- self.gcode += "M03\n" # Spindle start
- self.gcode += self.pausecode + "\n"
-
- for geo in geometry.solid_geometry:
-
- if type(geo) == Polygon:
- self.gcode += self.polygon2gcode(geo)
- continue
-
- if type(geo) == LineString or type(geo) == LinearRing:
- self.gcode += self.linear2gcode(geo)
- continue
-
- if type(geo) == Point:
- self.gcode += self.point2gcode(geo)
- continue
- if type(geo) == MultiPolygon:
- for poly in geo:
- self.gcode += self.polygon2gcode(poly)
- continue
- print "WARNING: G-code generation not implemented for %s" % (str(type(geo)))
-
- self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
- self.gcode += "G00 X0Y0\n"
- self.gcode += "M05\n" # Spindle stop
- def pre_parse(self, gtext):
- """
- gtext is a single string with g-code
- """
- # Units: G20-inches, G21-mm
- units_re = re.compile(r'^G2([01])')
- # TODO: This has to be re-done
- gcmds = []
- lines = gtext.split("\n") # TODO: This is probably a lot of work!
- for line in lines:
- # Clean up
- line = line.strip()
- # Remove comments
- # NOTE: Limited to 1 bracket pair
- op = line.find("(")
- cl = line.find(")")
- if op > -1 and cl > op:
- #comment = line[op+1:cl]
- line = line[:op] + line[(cl+1):]
- # Units
- match = units_re.match(line)
- if match:
- self.units = {'0': "IN", '1': "MM"}[match.group(1)]
- # Parse GCode
- # 0 4 12
- # G01 X-0.007 Y-0.057
- # --> codes_idx = [0, 4, 12]
- codes = "NMGXYZIJFP"
- codes_idx = []
- i = 0
- for ch in line:
- if ch in codes:
- codes_idx.append(i)
- i += 1
- n_codes = len(codes_idx)
- if n_codes == 0:
- continue
- # Separate codes in line
- parts = []
- for p in range(n_codes-1):
- parts.append(line[codes_idx[p]:codes_idx[p+1]].strip())
- parts.append(line[codes_idx[-1]:].strip())
- # Separate codes from values
- cmds = {}
- for part in parts:
- cmds[part[0]] = float(part[1:])
- gcmds.append(cmds)
- return gcmds
- def gcode_parse(self):
- """
- G-Code parser (from self.gcode). Generates dictionary with
- single-segment LineString's and "kind" indicating cut or travel,
- fast or feedrate speed.
- """
- # Results go here
- geometry = []
-
- # TODO: Merge into single parser?
- gobjs = self.pre_parse(self.gcode)
-
- # Last known instruction
- current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
-
- # Process every instruction
- for gobj in gobjs:
- if 'Z' in gobj:
- if ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']:
- print "WARNING: Non-orthogonal motion: From", current
- print " To:", gobj
- current['Z'] = gobj['Z']
-
- if 'G' in gobj:
- current['G'] = int(gobj['G'])
-
- if 'X' in gobj or 'Y' in gobj:
- x = 0
- y = 0
- kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
-
- if 'X' in gobj:
- x = gobj['X']
- else:
- x = current['X']
-
- if 'Y' in gobj:
- y = gobj['Y']
- else:
- y = current['Y']
-
- if current['Z'] > 0:
- kind[0] = 'T'
- if current['G'] > 0:
- kind[1] = 'S'
-
- arcdir = [None, None, "cw", "ccw"]
- if current['G'] in [0, 1]: # line
- geometry.append({'geom': LineString([(current['X'], current['Y']),
- (x, y)]), 'kind': kind})
- if current['G'] in [2, 3]: # arc
- center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
- radius = sqrt(gobj['I']**2 + gobj['J']**2)
- start = arctan2(-gobj['J'], -gobj['I'])
- stop = arctan2(-center[1]+y, -center[0]+x)
- geometry.append({'geom': arc(center, radius, start, stop,
- arcdir[current['G']],
- self.steps_per_circ),
- 'kind': kind})
- # Update current instruction
- for code in gobj:
- current[code] = gobj[code]
-
- #self.G_geometry = geometry
- self.gcode_parsed = geometry
- return geometry
-
- def plot(self, tooldia=None, dpi=75, margin=0.1,
- color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
- alpha={"T": 0.3, "C": 1.0}):
- """
- Creates a Matplotlib figure with a plot of the
- G-code job.
- """
- if tooldia is None:
- tooldia = self.tooldia
-
- fig = Figure(dpi=dpi)
- ax = fig.add_subplot(111)
- ax.set_aspect(1)
- xmin, ymin, xmax, ymax = self.input_geometry_bounds
- ax.set_xlim(xmin-margin, xmax+margin)
- ax.set_ylim(ymin-margin, ymax+margin)
-
- if tooldia == 0:
- for geo in self.gcode_parsed:
- linespec = '--'
- linecolor = color[geo['kind'][0]][1]
- if geo['kind'][0] == 'C':
- linespec = 'k-'
- x, y = geo['geom'].coords.xy
- ax.plot(x, y, linespec, color=linecolor)
- else:
- for geo in self.gcode_parsed:
- poly = geo['geom'].buffer(tooldia/2.0)
- patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
- edgecolor=color[geo['kind'][0]][1],
- alpha=alpha[geo['kind'][0]], zorder=2)
- ax.add_patch(patch)
-
- return fig
-
- def plot2(self, axes, tooldia=None, dpi=75, margin=0.1,
- color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
- alpha={"T": 0.3, "C":1.0}):
- """
- Plots the G-code job onto the given axes.
- """
- if tooldia is None:
- tooldia = self.tooldia
-
- if tooldia == 0:
- for geo in self.gcode_parsed:
- linespec = '--'
- linecolor = color[geo['kind'][0]][1]
- if geo['kind'][0] == 'C':
- linespec = 'k-'
- x, y = geo['geom'].coords.xy
- axes.plot(x, y, linespec, color=linecolor)
- else:
- for geo in self.gcode_parsed:
- poly = geo['geom'].buffer(tooldia/2.0)
- patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
- edgecolor=color[geo['kind'][0]][1],
- alpha=alpha[geo['kind'][0]], zorder=2)
- axes.add_patch(patch)
-
- def create_geometry(self):
- self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
- def polygon2gcode(self, polygon):
- """
- Creates G-Code for the exterior and all interior paths
- of a polygon.
- @param polygon: A Shapely.Polygon
- @type polygon: Shapely.Polygon
- """
- gcode = ""
- t = "G0%d X%.4fY%.4f\n"
- path = list(polygon.exterior.coords) # Polygon exterior
- gcode += t % (0, path[0][0], path[0][1]) # Move to first point
- gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
- for pt in path[1:]:
- gcode += t % (1, pt[0], pt[1]) # Linear motion to point
- gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
- for ints in polygon.interiors: # Polygon interiors
- path = list(ints.coords)
- gcode += t % (0, path[0][0], path[0][1]) # Move to first point
- gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
- for pt in path[1:]:
- gcode += t % (1, pt[0], pt[1]) # Linear motion to point
- gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
- return gcode
- def linear2gcode(self, linear):
- gcode = ""
- t = "G0%d X%.4fY%.4f\n"
- path = list(linear.coords)
- gcode += t%(0, path[0][0], path[0][1]) # Move to first point
- gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
- for pt in path[1:]:
- gcode += t % (1, pt[0], pt[1]) # Linear motion to point
- gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
- return gcode
- def point2gcode(self, point):
- gcode = ""
- t = "G0%d X%.4fY%.4f\n"
- path = list(point.coords)
- gcode += t % (0, path[0][0], path[0][1]) # Move to first point
- gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
- gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
- def scale(self, factor):
- """
- Scales all the geometry on the XY plane in the object by the
- given factor. Tool sizes, feedrates, or Z-axis dimensions are
- not altered.
- :param factor: Number by which to scale the object.
- :type factor: float
- :return: None
- :rtype: None
- """
- for g in self.gcode_parsed:
- g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
- def convert_units(self, units):
- factor = Geometry.convert_units(self, units)
- self.z_move *= factor
- self.z_cut *= factor
- self.feedrate *= factor
- self.tooldia *= factor
- return factor
- def get_bounds(geometry_set):
- xmin = Inf
- ymin = Inf
- xmax = -Inf
- ymax = -Inf
- print "Getting bounds of:", str(geometry_set)
- for gs in geometry_set:
- gxmin, gymin, gxmax, gymax = geometry_set[gs].bounds()
- xmin = min([xmin, gxmin])
- ymin = min([ymin, gymin])
- xmax = max([xmax, gxmax])
- ymax = max([ymax, gymax])
-
- return [xmin, ymin, xmax, ymax]
- def arc(center, radius, start, stop, direction, steps_per_circ):
- """
- Creates a Shapely.LineString for the specified arc.
- @param center: Coordinates of the center [x, y]
- @type center: list
- @param radius: Radius of the arc.
- @type radius: float
- @param start: Starting angle in radians
- @type start: float
- @param stop: End angle in radians
- @type stop: float
- @param direction: Orientation of the arc, "CW" or "CCW"
- @type direction: string
- @param steps_per_circ: Number of straight line segments to
- represent a circle.
- @type steps_per_circ: int
- """
- da_sign = {"cw": -1.0, "ccw": 1.0}
- points = []
- if direction == "ccw" and stop <= start:
- stop += 2*pi
- if direction == "cw" and stop >= start:
- stop -= 2*pi
-
- angle = abs(stop - start)
-
- #angle = stop-start
- steps = max([int(ceil(angle/(2*pi)*steps_per_circ)), 2])
- delta_angle = da_sign[direction]*angle*1.0/steps
- for i in range(steps+1):
- theta = start + delta_angle*i
- points.append([center[0]+radius*cos(theta), center[1]+radius*sin(theta)])
- return LineString(points)
- def clear_poly(poly, tooldia, overlap=0.1):
- """
- Creates a list of Shapely geometry objects covering the inside
- of a Shapely.Polygon. Use for removing all the copper in a region
- or bed flattening.
- @param poly: Target polygon
- @type poly: Shapely.Polygon
- @param tooldia: Diameter of the tool
- @type tooldia: float
- @param overlap: Fraction of the tool diameter to overlap
- in each pass.
- @type overlap: float
- @return list of Shapely.Polygon
- """
- poly_cuts = [poly.buffer(-tooldia/2.0)]
- while True:
- poly = poly_cuts[-1].buffer(-tooldia*(1-overlap))
- if poly.area > 0:
- poly_cuts.append(poly)
- else:
- break
- return poly_cuts
- def find_polygon(poly_set, point):
- """
- Return the first polygon in the list of polygons poly_set
- that contains the given point.
- """
- p = Point(point)
- for poly in poly_set:
- if poly.contains(p):
- return poly
- return None
- ############### cam.py ####################
- def coord(gstr, digits, fraction):
- """
- Parse Gerber coordinates
- """
- global gerbx, gerby
- xindex = gstr.find("X")
- yindex = gstr.find("Y")
- index = gstr.find("D")
- if xindex == -1:
- x = gerbx
- y = int(gstr[(yindex+1):index])*(10**(-fraction))
- elif yindex == -1:
- y = gerby
- x = int(gstr[(xindex+1):index])*(10**(-fraction))
- else:
- x = int(gstr[(xindex+1):yindex])*(10**(-fraction))
- y = int(gstr[(yindex+1):index])*(10**(-fraction))
- gerbx = x
- gerby = y
- return [x, y]
- ################ end of cam.py #############
|