| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713 |
- #import cairo
- #from string import *
- #from math import *
- #from random import *
- #from struct import *
- #import os
- #import sys
- from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
- from matplotlib.figure import Figure
- # 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
- 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 == 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 == 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 == 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(1):
- polygon = poly_cuts[-1].buffer(-tooldia*(1-overlap))
- if polygon.area > 0:
- poly_cuts.append(polygon)
- else:
- break
- return poly_cuts
-
- class Gerber (Geometry):
- def __init__(self):
- # Initialize parent
- Geometry.__init__(self)
-
- # Number format
- self.digits = 3
- self.fraction = 4
-
- ## Gerber elements ##
- # Apertures {'id':{'type':chr,
- # ['size':float], ['width':float],
- # ['height':float]}, ...}
- self.apertures = {}
-
- # Paths [{'linestring':LineString, 'aperture':dict}]
- self.paths = []
-
- # Buffered Paths [Polygon]
- # Paths transformed into Polygons by
- # offsetting the aperture size/2
- self.buffered_paths = []
-
- # Polygon regions [{'polygon':Polygon, 'aperture':dict}]
- self.regions = []
-
- # Flashes [{'loc':[float,float], 'aperture':dict}]
- self.flashes = []
-
- # Geometry from flashes
- self.flash_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 region['polygon'].is_valid == False:
- 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.
- '''
- 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.
- '''
- 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
- print "WARNING: Line ignored:", gline
-
- if len(path) > 1:
- # EOF, create shapely LineString if something 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):
- if len(self.buffered_paths) == 0:
- 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):
- def __init__(self):
- Geometry.__init__(self)
-
- self.tools = {}
-
- self.drills = []
-
- 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.
- '''
- 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 > indexF:
- tool = eline[1:indexC]
- spec = eline[indexC+1:indexF]
- self.tools[tool] = 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] = 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
-
- 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)
- class CNCjob(Geometry):
- def __init__(self, units="in", kind="generic", z_move = 0.1,
- feedrate = 3.0, z_cut = -0.002):
-
- # Options
- self.kind = kind
- self.units = units
- self.z_cut = z_cut
- self.z_move = z_move
- self.feedrate = feedrate
-
- # Constants
- self.unitcode = {"in": "G20", "mm": "G21"}
- self.pausecode = "G04 P1"
- self.feedminutecode = "G94"
- self.absolutecode = "G90"
-
- # Input/Output G-Code
- self.gcode = ""
-
- # Bounds of geometry given to CNCjob.generate_from_geometry()
- self.input_geometry_bounds = None
-
- # Tool diameter given to CNCjob.generate_from_geometry()
- self.tooldia = 0
-
- # Output generated by CNCjob.create_gcode_geometry()
- #self.G_geometry = None
- self.gcode_parsed = None
-
- def generate_from_excellon(self, exobj):
- '''
- Generates G-code for drilling from excellon text.
- 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 = []
- gcode = ""
-
- for drill in exobj.drill:
- if drill['tool'] == tool:
- points.append(drill['point'])
-
- gcode = self.unitcode[self.units] + "\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_geometry(self, geometry, append=True, tooldia=None):
- '''
- Generates G-Code for geometry (Shapely collection).
- '''
- if tooldia == None:
- tooldia = self.tooldia
- else:
- self.tooldia = tooldia
-
- self.input_geometry_bounds = geometry.bounds
-
- if append == False:
- self.gcode = ""
- t = "G0%d X%.4fY%.4f\n"
- self.gcode = self.unitcode[self.units] + "\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:
-
- if type(geo) == Polygon:
- path = list(geo.exterior.coords) # Polygon exterior
- self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point
- self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting
- for pt in path[1:]:
- self.gcode += t%(1, pt[0], pt[1]) # Linear motion to point
- self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting
- for ints in geo.interiors: # Polygon interiors
- path = list(ints.coords)
- self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point
- self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting
- for pt in path[1:]:
- self.gcode += t%(1, pt[0], pt[1]) # Linear motion to point
- self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting
- continue
-
- if type(geo) == LineString or type(geo) == LinearRing:
- path = list(geo.coords)
- self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point
- self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting
- for pt in path[1:]:
- self.gcode += t%(1, pt[0], pt[1]) # Linear motion to point
- self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting
- continue
-
- if type(geo) == Point:
- path = list(geo.coords)
- self.gcode += t%(0, path[0][0], path[0][1]) # Move to first point
- self.gcode += "G01 Z%.4f\n"%self.z_cut # Start cutting
- self.gcode += "G00 Z%.4f\n"%self.z_move # Stop cutting
- 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 gcode_parse(self):
- steps_per_circ = 20
- '''
- G-Code parser (from self.gcode). Generates dictionary with
- single-segment LineString's and "kind" indicating cut or travel,
- fast or feedrate speed.
- '''
- geometry = []
-
- # TODO: ???? bring this into the class??
- gobjs = gparse1b(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']],
- 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 == 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 == 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 gparse1b(gtext):
- '''
- gtext is a single string with g-code
- '''
- gcmds = []
- lines = gtext.split("\n") # TODO: This is probably a lot of work!
- for line in lines:
- 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):]
-
- # 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 get_bounds(geometry_set):
- xmin = Inf
- ymin = Inf
- xmax = -Inf
- ymax = -Inf
-
- 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):
- 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)
-
- ############### 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 #############
|