2014-02-06 00:36:47 +00:00
|
|
|
############################################################
|
2014-02-19 05:21:29 +00:00
|
|
|
# FlatCAM: 2D Post-processing for Manufacturing #
|
|
|
|
# http://caram.cl/software/flatcam #
|
|
|
|
# Author: Juan Pablo Caram (c) #
|
2014-02-06 00:36:47 +00:00
|
|
|
# Date: 2/5/2014 #
|
2014-02-19 05:21:29 +00:00
|
|
|
# MIT Licence #
|
2014-02-06 00:36:47 +00:00
|
|
|
############################################################
|
|
|
|
|
2014-01-08 06:46:50 +00:00
|
|
|
from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
|
2014-01-05 01:54:30 +00:00
|
|
|
from matplotlib.figure import Figure
|
2014-01-27 03:58:18 +00:00
|
|
|
import re
|
2014-01-03 04:14:25 +00:00
|
|
|
|
|
|
|
# See: http://toblerity.org/shapely/manual.html
|
2014-01-10 03:14:46 +00:00
|
|
|
from shapely.geometry import Polygon, LineString, Point, LinearRing
|
2014-01-03 04:14:25 +00:00
|
|
|
from shapely.geometry import MultiPoint, MultiPolygon
|
|
|
|
from shapely.geometry import box as shply_box
|
|
|
|
from shapely.ops import cascaded_union
|
2014-01-27 03:58:18 +00:00
|
|
|
import shapely.affinity as affinity
|
2014-01-31 05:39:34 +00:00
|
|
|
from shapely.wkt import loads as sloads
|
|
|
|
from shapely.wkt import dumps as sdumps
|
|
|
|
from shapely.geometry.base import BaseGeometry
|
2014-01-03 04:14:25 +00:00
|
|
|
|
2014-01-13 06:25:57 +00:00
|
|
|
# Used for solid polygons in Matplotlib
|
2014-01-08 06:46:50 +00:00
|
|
|
from descartes.patch import PolygonPatch
|
2014-01-03 04:14:25 +00:00
|
|
|
|
2014-01-31 05:39:34 +00:00
|
|
|
import simplejson as json
|
2014-02-12 02:50:03 +00:00
|
|
|
# TODO: Commented for FlatCAM packaging with cx_freeze
|
|
|
|
#from matplotlib.pyplot import plot
|
2014-01-13 06:25:57 +00:00
|
|
|
|
2014-02-19 05:21:29 +00:00
|
|
|
|
2014-01-04 21:20:57 +00:00
|
|
|
class Geometry:
|
2014-01-03 04:14:25 +00:00
|
|
|
def __init__(self):
|
|
|
|
# Units (in or mm)
|
2014-01-04 21:20:57 +00:00
|
|
|
self.units = 'in'
|
|
|
|
|
|
|
|
# Final geometry: MultiPolygon
|
|
|
|
self.solid_geometry = None
|
2014-01-31 05:39:34 +00:00
|
|
|
|
|
|
|
# Attributes to be included in serialization
|
|
|
|
self.ser_attrs = ['units', 'solid_geometry']
|
2014-01-04 21:20:57 +00:00
|
|
|
|
|
|
|
def isolation_geometry(self, offset):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-04 21:20:57 +00:00
|
|
|
Creates contours around geometry at a given
|
|
|
|
offset distance.
|
2014-02-22 05:52:25 +00:00
|
|
|
|
|
|
|
:param offset: Offset distance.
|
|
|
|
:type offset: float
|
|
|
|
:return: The buffered geometry.
|
|
|
|
:rtype: Shapely.MultiPolygon or Shapely.Polygon
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-04 21:20:57 +00:00
|
|
|
return self.solid_geometry.buffer(offset)
|
|
|
|
|
|
|
|
def bounds(self):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-04 21:20:57 +00:00
|
|
|
Returns coordinates of rectangular bounds
|
|
|
|
of geometry: (xmin, ymin, xmax, ymax).
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
|
|
|
if self.solid_geometry is None:
|
2014-01-04 21:20:57 +00:00
|
|
|
print "Warning: solid_geometry not computed yet."
|
2014-01-13 06:25:57 +00:00
|
|
|
return (0, 0, 0, 0)
|
2014-01-10 03:14:46 +00:00
|
|
|
|
|
|
|
if type(self.solid_geometry) == list:
|
2014-02-01 19:23:31 +00:00
|
|
|
# TODO: This can be done faster. See comment from Shapely mailing lists.
|
2014-01-10 03:14:46 +00:00
|
|
|
return cascaded_union(self.solid_geometry).bounds
|
|
|
|
else:
|
|
|
|
return self.solid_geometry.bounds
|
2014-01-04 21:20:57 +00:00
|
|
|
|
|
|
|
def size(self):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-04 21:20:57 +00:00
|
|
|
Returns (width, height) of rectangular
|
|
|
|
bounds of geometry.
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
|
|
|
if self.solid_geometry is None:
|
2014-01-04 21:20:57 +00:00
|
|
|
print "Warning: solid_geometry not computed yet."
|
|
|
|
return 0
|
|
|
|
bounds = self.bounds()
|
|
|
|
return (bounds[2]-bounds[0], bounds[3]-bounds[1])
|
2014-01-06 02:45:08 +00:00
|
|
|
|
|
|
|
def get_empty_area(self, boundary=None):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-06 02:45:08 +00:00
|
|
|
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.
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
|
|
|
if boundary is None:
|
2014-01-06 02:45:08 +00:00
|
|
|
boundary = self.solid_geometry.envelope
|
2014-01-10 03:14:46 +00:00
|
|
|
return boundary.difference(self.solid_geometry)
|
2014-01-06 02:45:08 +00:00
|
|
|
|
2014-01-13 06:25:57 +00:00
|
|
|
def clear_polygon(self, polygon, tooldia, overlap=0.15):
|
|
|
|
"""
|
2014-01-06 02:45:08 +00:00
|
|
|
Creates geometry inside a polygon for a tool to cover
|
|
|
|
the whole area.
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-06 02:45:08 +00:00
|
|
|
poly_cuts = [polygon.buffer(-tooldia/2.0)]
|
2014-01-13 06:25:57 +00:00
|
|
|
while True:
|
2014-01-06 02:45:08 +00:00
|
|
|
polygon = poly_cuts[-1].buffer(-tooldia*(1-overlap))
|
|
|
|
if polygon.area > 0:
|
|
|
|
poly_cuts.append(polygon)
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
return poly_cuts
|
2014-01-27 03:58:18 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2014-02-22 05:52:25 +00:00
|
|
|
def offset(self, vect):
|
|
|
|
"""
|
|
|
|
Offset the geometry by the given vector. Override this method.
|
|
|
|
|
|
|
|
:param vect: (x, y) vector by which to offset the object.
|
|
|
|
:type vect: tuple
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
return
|
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
def convert_units(self, units):
|
|
|
|
"""
|
|
|
|
Converts the units of the object to ``units`` by scaling all
|
2014-02-01 19:23:31 +00:00
|
|
|
the geometry appropriately. This call ``scale()``. Don't call
|
|
|
|
it again in descendents.
|
2014-01-27 03:58:18 +00:00
|
|
|
|
|
|
|
:param units: "IN" or "MM"
|
|
|
|
:type units: str
|
|
|
|
:return: Scaling factor resulting from unit change.
|
|
|
|
:rtype: float
|
|
|
|
"""
|
2014-02-01 19:23:31 +00:00
|
|
|
print "Geometry.convert_units()"
|
2014-01-27 03:58:18 +00:00
|
|
|
|
|
|
|
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
|
2014-01-31 05:39:34 +00:00
|
|
|
|
|
|
|
def to_dict(self):
|
|
|
|
"""
|
|
|
|
Returns a respresentation of the object as a dictionary.
|
2014-02-01 01:50:54 +00:00
|
|
|
Attributes to include are listed in ``self.ser_attrs``.
|
2014-01-31 05:39:34 +00:00
|
|
|
|
|
|
|
:return: A dictionary-encoded copy of the object.
|
|
|
|
:rtype: dict
|
|
|
|
"""
|
|
|
|
d = {}
|
|
|
|
for attr in self.ser_attrs:
|
|
|
|
d[attr] = getattr(self, attr)
|
|
|
|
return d
|
|
|
|
|
|
|
|
def from_dict(self, d):
|
2014-02-01 01:50:54 +00:00
|
|
|
"""
|
|
|
|
Sets object's attributes from a dictionary.
|
|
|
|
Attributes to include are listed in ``self.ser_attrs``.
|
2014-03-01 00:32:04 +00:00
|
|
|
This method will look only for only and all the
|
|
|
|
attributes in ``self.ser_attrs``. They must all
|
|
|
|
be present. Use only for deserializing saved
|
|
|
|
objects.
|
|
|
|
|
|
|
|
:param d: Dictionary of attributes to set in the object.
|
|
|
|
:type d: dict
|
|
|
|
:return: None
|
2014-02-01 01:50:54 +00:00
|
|
|
"""
|
|
|
|
for attr in self.ser_attrs:
|
|
|
|
setattr(self, attr, d[attr])
|
2014-01-31 05:39:34 +00:00
|
|
|
|
2014-01-04 21:20:57 +00:00
|
|
|
|
|
|
|
class Gerber (Geometry):
|
2014-01-27 03:58:18 +00:00
|
|
|
"""
|
|
|
|
**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()``.
|
2014-02-06 00:36:47 +00:00
|
|
|
|
2014-02-12 02:50:03 +00:00
|
|
|
**USAGE**::
|
|
|
|
|
|
|
|
g = Gerber()
|
|
|
|
g.parse_file(filename)
|
|
|
|
g.create_geometry()
|
|
|
|
do_something(s.solid_geometry)
|
2014-02-06 00:36:47 +00:00
|
|
|
|
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
"""
|
|
|
|
|
2014-01-04 21:20:57 +00:00
|
|
|
def __init__(self):
|
2014-02-06 00:36:47 +00:00
|
|
|
"""
|
|
|
|
The constructor takes no parameters. Use ``gerber.parse_files()``
|
|
|
|
or ``gerber.parse_lines()`` to populate the object from Gerber source.
|
|
|
|
:return: Gerber object
|
|
|
|
:rtype: Gerber
|
|
|
|
"""
|
2014-01-04 21:20:57 +00:00
|
|
|
# Initialize parent
|
|
|
|
Geometry.__init__(self)
|
2014-01-03 04:14:25 +00:00
|
|
|
|
|
|
|
# Number format
|
2014-02-19 05:21:29 +00:00
|
|
|
self.int_digits = 3
|
2014-01-27 03:58:18 +00:00
|
|
|
"""Number of integer digits in Gerber numbers. Used during parsing."""
|
|
|
|
|
2014-02-19 05:21:29 +00:00
|
|
|
self.frac_digits = 4
|
2014-01-27 03:58:18 +00:00
|
|
|
"""Number of fraction digits in Gerber numbers. Used during parsing."""
|
2014-01-03 04:14:25 +00:00
|
|
|
|
|
|
|
## Gerber elements ##
|
|
|
|
# Apertures {'id':{'type':chr,
|
|
|
|
# ['size':float], ['width':float],
|
|
|
|
# ['height':float]}, ...}
|
|
|
|
self.apertures = {}
|
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
# Paths [{'linestring':LineString, 'aperture':str}]
|
2014-01-03 04:14:25 +00:00
|
|
|
self.paths = []
|
|
|
|
|
|
|
|
# Buffered Paths [Polygon]
|
|
|
|
# Paths transformed into Polygons by
|
|
|
|
# offsetting the aperture size/2
|
|
|
|
self.buffered_paths = []
|
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
# Polygon regions [{'polygon':Polygon, 'aperture':str}]
|
2014-01-03 04:14:25 +00:00
|
|
|
self.regions = []
|
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
# Flashes [{'loc':[float,float], 'aperture':str}]
|
2014-01-03 04:14:25 +00:00
|
|
|
self.flashes = []
|
|
|
|
|
2014-01-07 03:08:55 +00:00
|
|
|
# Geometry from flashes
|
|
|
|
self.flash_geometry = []
|
2014-01-27 03:58:18 +00:00
|
|
|
|
2014-01-31 05:39:34 +00:00
|
|
|
# Attributes to be included in serialization
|
|
|
|
# Always append to it because it carries contents
|
|
|
|
# from Geometry.
|
2014-02-19 05:21:29 +00:00
|
|
|
self.ser_attrs += ['int_digits', 'frac_digits', 'apertures', 'paths',
|
2014-01-31 05:39:34 +00:00
|
|
|
'buffered_paths', 'regions', 'flashes',
|
|
|
|
'flash_geometry']
|
|
|
|
|
2014-02-19 05:21:29 +00:00
|
|
|
#### Parser patterns ####
|
|
|
|
# FS - Format Specification
|
|
|
|
# The format of X and Y must be the same!
|
|
|
|
# L-omit leading zeros, T-omit trailing zeros
|
|
|
|
# A-absolute notation, I-incremental notation
|
|
|
|
self.fmt_re = re.compile(r'%FS([LT])([AI])X(\d)(\d)Y\d\d\*%$')
|
|
|
|
|
|
|
|
# Mode (IN/MM)
|
|
|
|
self.mode_re = re.compile(r'^%MO(IN|MM)\*%$')
|
|
|
|
|
|
|
|
# Comment G04|G4
|
|
|
|
self.comm_re = re.compile(r'^G0?4(.*)$')
|
|
|
|
|
|
|
|
# AD - Aperture definition
|
|
|
|
self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z0-9]*),(.*)\*%$')
|
|
|
|
|
|
|
|
# AM - Aperture Macro
|
|
|
|
# Beginning of macro (Ends with *%):
|
|
|
|
self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*')
|
|
|
|
|
|
|
|
# Tool change
|
|
|
|
# May begin with G54 but that is deprecated
|
|
|
|
self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$')
|
|
|
|
|
|
|
|
# G01 - Linear interpolation plus flashes
|
|
|
|
# Operation code (D0x) missing is deprecated... oh well I will support it.
|
|
|
|
self.lin_re = re.compile(r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$')
|
|
|
|
|
|
|
|
self.setlin_re = re.compile(r'^(?:G0?1)\*')
|
|
|
|
|
|
|
|
# G02/3 - Circular interpolation
|
|
|
|
# 2-clockwise, 3-counterclockwise
|
|
|
|
self.circ_re = re.compile(r'^(?:G0?([23]))?(?:X(-?\d+))?(?:Y(-?\d+))' +
|
|
|
|
'?(?:I(-?\d+))?(?:J(-?\d+))?D0([12])\*$')
|
|
|
|
|
|
|
|
# G01/2/3 Occurring without coordinates
|
|
|
|
self.interp_re = re.compile(r'^(?:G0?([123]))\*')
|
|
|
|
|
|
|
|
# Single D74 or multi D75 quadrant for circular interpolation
|
|
|
|
self.quad_re = re.compile(r'^G7([45])\*$')
|
|
|
|
|
|
|
|
# Region mode on
|
|
|
|
# In region mode, D01 starts a region
|
|
|
|
# and D02 ends it. A new region can be started again
|
|
|
|
# with D01. All contours must be closed before
|
|
|
|
# D02 or G37.
|
|
|
|
self.regionon_re = re.compile(r'^G36\*$')
|
|
|
|
|
|
|
|
# Region mode off
|
|
|
|
# Will end a region and come off region mode.
|
|
|
|
# All contours must be closed before D02 or G37.
|
|
|
|
self.regionoff_re = re.compile(r'^G37\*$')
|
|
|
|
|
|
|
|
# End of file
|
|
|
|
self.eof_re = re.compile(r'^M02\*')
|
|
|
|
|
|
|
|
# IP - Image polarity
|
|
|
|
self.pol_re = re.compile(r'^%IP(POS|NEG)\*%$')
|
|
|
|
|
|
|
|
# LP - Level polarity
|
|
|
|
self.lpol_re = re.compile(r'^%LP([DC])\*%$')
|
|
|
|
|
2014-02-27 05:12:49 +00:00
|
|
|
# TODO: This is bad.
|
|
|
|
self.steps_per_circ = 40
|
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
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
|
2014-03-04 03:12:07 +00:00
|
|
|
#print "Scaling apertures..."
|
2014-01-27 03:58:18 +00:00
|
|
|
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
|
2014-03-04 03:12:07 +00:00
|
|
|
#print "Scaling paths..."
|
2014-01-27 03:58:18 +00:00
|
|
|
for path in self.paths:
|
|
|
|
path['linestring'] = affinity.scale(path['linestring'],
|
|
|
|
factor, factor, origin=(0, 0))
|
|
|
|
|
|
|
|
# Flashes
|
2014-03-04 03:12:07 +00:00
|
|
|
#print "Scaling flashes..."
|
2014-01-27 03:58:18 +00:00
|
|
|
for fl in self.flashes:
|
|
|
|
# TODO: Shouldn't 'loc' be a numpy.array()?
|
|
|
|
fl['loc'][0] *= factor
|
|
|
|
fl['loc'][1] *= factor
|
|
|
|
|
|
|
|
# Regions
|
2014-03-04 03:12:07 +00:00
|
|
|
#print "Scaling regions..."
|
2014-01-27 03:58:18 +00:00
|
|
|
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()
|
|
|
|
|
2014-02-22 05:52:25 +00:00
|
|
|
def offset(self, vect):
|
|
|
|
"""
|
|
|
|
Offsets the objects' geometry on the XY plane by a given vector.
|
|
|
|
These are:
|
|
|
|
|
|
|
|
* ``paths``
|
|
|
|
* ``regions``
|
|
|
|
* ``flashes``
|
|
|
|
|
|
|
|
Then ``buffered_paths``, ``flash_geometry`` and ``solid_geometry``
|
|
|
|
are re-created with ``self.create_geometry()``.
|
|
|
|
:param vect: (x, y) offset vector.
|
|
|
|
:type vect: tuple
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
|
|
|
|
dx, dy = vect
|
|
|
|
|
|
|
|
# Paths
|
2014-03-04 03:12:07 +00:00
|
|
|
#print "Shifting paths..."
|
2014-02-22 05:52:25 +00:00
|
|
|
for path in self.paths:
|
|
|
|
path['linestring'] = affinity.translate(path['linestring'],
|
|
|
|
xoff=dx, yoff=dy)
|
|
|
|
|
|
|
|
# Flashes
|
2014-03-04 03:12:07 +00:00
|
|
|
#print "Shifting flashes..."
|
2014-02-22 05:52:25 +00:00
|
|
|
for fl in self.flashes:
|
|
|
|
# TODO: Shouldn't 'loc' be a numpy.array()?
|
|
|
|
fl['loc'][0] += dx
|
|
|
|
fl['loc'][1] += dy
|
|
|
|
|
|
|
|
# Regions
|
2014-03-04 03:12:07 +00:00
|
|
|
#print "Shifting regions..."
|
2014-02-22 05:52:25 +00:00
|
|
|
for reg in self.regions:
|
|
|
|
reg['polygon'] = affinity.translate(reg['polygon'],
|
|
|
|
xoff=dx, yoff=dy)
|
|
|
|
|
|
|
|
# Now buffered_paths, flash_geometry and solid_geometry
|
|
|
|
self.create_geometry()
|
|
|
|
|
2014-01-03 04:14:25 +00:00
|
|
|
def fix_regions(self):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-03 04:14:25 +00:00
|
|
|
Overwrites the region polygons with fixed
|
|
|
|
versions if found to be invalid (according to Shapely).
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-02-22 05:52:25 +00:00
|
|
|
|
2014-01-03 04:14:25 +00:00
|
|
|
for region in self.regions:
|
2014-01-13 06:25:57 +00:00
|
|
|
if not region['polygon'].is_valid:
|
2014-01-03 04:14:25 +00:00
|
|
|
region['polygon'] = region['polygon'].buffer(0)
|
|
|
|
|
|
|
|
def buffer_paths(self):
|
2014-02-22 05:52:25 +00:00
|
|
|
"""
|
|
|
|
This is part of the parsing process. "Thickens" the paths
|
|
|
|
by their appertures. This will only work for circular appertures.
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
|
2014-01-03 04:14:25 +00:00
|
|
|
self.buffered_paths = []
|
|
|
|
for path in self.paths:
|
2014-02-27 05:12:49 +00:00
|
|
|
try:
|
|
|
|
width = self.apertures[path["aperture"]]["size"]
|
|
|
|
self.buffered_paths.append(path["linestring"].buffer(width/2))
|
|
|
|
except KeyError:
|
|
|
|
print "ERROR: Failed to buffer path: ", path
|
|
|
|
print "Apertures: ", self.apertures
|
2014-01-03 04:14:25 +00:00
|
|
|
|
|
|
|
def aperture_parse(self, gline):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-27 03:58:18 +00:00
|
|
|
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)
|
2014-02-12 02:50:03 +00:00
|
|
|
* *Obround (O)*: width (float), height (float).
|
|
|
|
|
|
|
|
:param gline: Line of Gerber code known to have an aperture definition.
|
|
|
|
:type gline: str
|
|
|
|
:return: Identifier of the aperture.
|
|
|
|
:rtype: str
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-02-19 05:21:29 +00:00
|
|
|
|
2014-01-03 04:14:25 +00:00
|
|
|
indexstar = gline.find("*")
|
2014-01-20 18:08:36 +00:00
|
|
|
indexc = gline.find("C,")
|
|
|
|
if indexc != -1: # Circle, example: %ADD11C,0.1*%
|
2014-02-19 05:21:29 +00:00
|
|
|
# Found some Gerber with a leading zero in the aperture id and the
|
|
|
|
# referenced it without the zero, so this is a hack to handle that.
|
|
|
|
apid = str(int(gline[4:indexc]))
|
2014-01-13 06:25:57 +00:00
|
|
|
self.apertures[apid] = {"type": "C",
|
2014-01-20 18:08:36 +00:00
|
|
|
"size": float(gline[indexc+2:indexstar])}
|
2014-01-03 04:14:25 +00:00
|
|
|
return apid
|
2014-01-20 18:08:36 +00:00
|
|
|
indexr = gline.find("R,")
|
|
|
|
if indexr != -1: # Rectangle, example: %ADD15R,0.05X0.12*%
|
2014-02-19 05:21:29 +00:00
|
|
|
# Hack explained above
|
|
|
|
apid = str(int(gline[4:indexr]))
|
2014-01-20 18:08:36 +00:00
|
|
|
indexx = gline.find("X")
|
2014-01-13 06:25:57 +00:00
|
|
|
self.apertures[apid] = {"type": "R",
|
2014-01-20 18:08:36 +00:00
|
|
|
"width": float(gline[indexr+2:indexx]),
|
|
|
|
"height": float(gline[indexx+1:indexstar])}
|
2014-01-03 04:14:25 +00:00
|
|
|
return apid
|
2014-01-20 18:08:36 +00:00
|
|
|
indexo = gline.find("O,")
|
|
|
|
if indexo != -1: # Obround
|
2014-02-19 05:21:29 +00:00
|
|
|
# Hack explained above
|
|
|
|
apid = str(int(gline[4:indexo]))
|
2014-01-20 18:08:36 +00:00
|
|
|
indexx = gline.find("X")
|
2014-01-13 06:25:57 +00:00
|
|
|
self.apertures[apid] = {"type": "O",
|
2014-01-20 18:08:36 +00:00
|
|
|
"width": float(gline[indexo+2:indexx]),
|
|
|
|
"height": float(gline[indexx+1:indexstar])}
|
2014-01-03 04:14:25 +00:00
|
|
|
return apid
|
2014-01-04 05:47:57 +00:00
|
|
|
print "WARNING: Aperture not implemented:", gline
|
2014-01-03 04:14:25 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
def parse_file(self, filename):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-06 02:45:08 +00:00
|
|
|
Calls Gerber.parse_lines() with array of lines
|
|
|
|
read from the given file.
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-03 04:14:25 +00:00
|
|
|
gfile = open(filename, 'r')
|
|
|
|
gstr = gfile.readlines()
|
|
|
|
gfile.close()
|
|
|
|
self.parse_lines(gstr)
|
|
|
|
|
|
|
|
def parse_lines(self, glines):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-02-19 05:21:29 +00:00
|
|
|
Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
|
|
|
|
``self.flashes``, ``self.regions`` and ``self.units``.
|
2014-01-27 03:58:18 +00:00
|
|
|
|
2014-03-01 00:32:04 +00:00
|
|
|
:param glines: Gerber code as list of strings, each element being
|
|
|
|
one line of the source file.
|
2014-02-19 05:21:29 +00:00
|
|
|
:type glines: list
|
|
|
|
:return: None
|
|
|
|
:rtype: None
|
|
|
|
"""
|
2014-01-27 03:58:18 +00:00
|
|
|
|
2014-02-27 05:12:49 +00:00
|
|
|
path = [] # Coordinates of the current path, each is [x, y]
|
2014-02-19 05:21:29 +00:00
|
|
|
|
2014-01-03 04:14:25 +00:00
|
|
|
last_path_aperture = None
|
|
|
|
current_aperture = None
|
2014-02-19 05:21:29 +00:00
|
|
|
|
|
|
|
# 1,2 or 3 from "G01", "G02" or "G03"
|
|
|
|
current_interpolation_mode = None
|
|
|
|
|
|
|
|
# 1 or 2 from "D01" or "D02"
|
|
|
|
# Note this is to support deprecated Gerber not putting
|
|
|
|
# an operation code at the end of every coordinate line.
|
|
|
|
current_operation_code = None
|
|
|
|
|
|
|
|
# Current coordinates
|
|
|
|
current_x = None
|
|
|
|
current_y = None
|
|
|
|
|
2014-02-27 05:12:49 +00:00
|
|
|
# How to interprest circular interpolation: SINGLE or MULTI
|
|
|
|
quadrant_mode = None
|
|
|
|
|
|
|
|
line_num = 0
|
2014-01-03 04:14:25 +00:00
|
|
|
for gline in glines:
|
2014-02-27 05:12:49 +00:00
|
|
|
line_num += 1
|
2014-02-19 05:21:29 +00:00
|
|
|
|
2014-02-27 05:12:49 +00:00
|
|
|
## G01 - Linear interpolation plus flashes
|
|
|
|
# Operation code (D0x) missing is deprecated... oh well I will support it.
|
2014-02-19 05:21:29 +00:00
|
|
|
match = self.lin_re.search(gline)
|
|
|
|
if match:
|
2014-02-27 05:12:49 +00:00
|
|
|
# Dxx alone? Will ignore for now.
|
|
|
|
if match.group(1) is None and match.group(2) is None and match.group(3) is None:
|
|
|
|
try:
|
|
|
|
current_operation_code = int(match.group(4))
|
|
|
|
except:
|
|
|
|
pass # A line with just * will match too.
|
|
|
|
continue
|
|
|
|
|
2014-02-19 05:21:29 +00:00
|
|
|
# Parse coordinates
|
|
|
|
if match.group(2) is not None:
|
|
|
|
current_x = parse_gerber_number(match.group(2), self.frac_digits)
|
|
|
|
if match.group(3) is not None:
|
|
|
|
current_y = parse_gerber_number(match.group(3), self.frac_digits)
|
|
|
|
|
|
|
|
# Parse operation code
|
|
|
|
if match.group(4) is not None:
|
2014-02-27 05:12:49 +00:00
|
|
|
current_operation_code = int(match.group(4))
|
2014-02-19 05:21:29 +00:00
|
|
|
|
|
|
|
# Pen down: add segment
|
2014-02-27 05:12:49 +00:00
|
|
|
if current_operation_code == 1:
|
2014-02-19 05:21:29 +00:00
|
|
|
path.append([current_x, current_y])
|
|
|
|
last_path_aperture = current_aperture
|
|
|
|
|
|
|
|
# Pen up: finish path
|
2014-02-27 05:12:49 +00:00
|
|
|
elif current_operation_code == 2:
|
2014-02-19 05:21:29 +00:00
|
|
|
if len(path) > 1:
|
2014-02-27 05:12:49 +00:00
|
|
|
if last_path_aperture is None:
|
|
|
|
print "Warning: No aperture defined for curent path. (%d)" % line_num
|
2014-02-19 05:21:29 +00:00
|
|
|
self.paths.append({"linestring": LineString(path),
|
|
|
|
"aperture": last_path_aperture})
|
2014-02-27 05:12:49 +00:00
|
|
|
path = [[current_x, current_y]] # Start new path
|
2014-02-19 05:21:29 +00:00
|
|
|
|
|
|
|
# Flash
|
2014-02-27 05:12:49 +00:00
|
|
|
elif current_operation_code == 3:
|
2014-02-19 05:21:29 +00:00
|
|
|
self.flashes.append({"loc": [current_x, current_y],
|
|
|
|
"aperture": current_aperture})
|
|
|
|
|
2014-01-03 04:14:25 +00:00
|
|
|
continue
|
2014-02-19 05:21:29 +00:00
|
|
|
|
2014-02-27 05:12:49 +00:00
|
|
|
## G02/3 - Circular interpolation
|
|
|
|
# 2-clockwise, 3-counterclockwise
|
|
|
|
match = self.circ_re.search(gline)
|
|
|
|
if match:
|
|
|
|
|
|
|
|
mode, x, y, i, j, d = match.groups()
|
|
|
|
try:
|
|
|
|
x = parse_gerber_number(x, self.frac_digits)
|
|
|
|
except:
|
|
|
|
x = current_x
|
|
|
|
try:
|
|
|
|
y = parse_gerber_number(y, self.frac_digits)
|
|
|
|
except:
|
|
|
|
y = current_y
|
|
|
|
try:
|
|
|
|
i = parse_gerber_number(i, self.frac_digits)
|
|
|
|
except:
|
|
|
|
i = 0
|
|
|
|
try:
|
|
|
|
j = parse_gerber_number(j, self.frac_digits)
|
|
|
|
except:
|
|
|
|
j = 0
|
|
|
|
|
|
|
|
if quadrant_mode is None:
|
|
|
|
print "ERROR: Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num
|
|
|
|
print gline
|
|
|
|
continue
|
|
|
|
|
|
|
|
if mode is None and current_interpolation_mode not in [2, 3]:
|
|
|
|
print "ERROR: Found arc without circular interpolation mode defined. (%d)" % line_num
|
|
|
|
print gline
|
|
|
|
continue
|
|
|
|
elif mode is not None:
|
|
|
|
current_interpolation_mode = int(mode)
|
|
|
|
|
|
|
|
# Set operation code if provided
|
|
|
|
if d is not None:
|
|
|
|
current_operation_code = int(d)
|
|
|
|
|
|
|
|
# Nothing created! Pen Up.
|
|
|
|
if current_operation_code == 2:
|
|
|
|
print "Warning: Arc with D2. (%d)" % line_num
|
|
|
|
if len(path) > 1:
|
|
|
|
if last_path_aperture is None:
|
|
|
|
print "Warning: No aperture defined for curent path. (%d)" % line_num
|
|
|
|
self.paths.append({"linestring": LineString(path),
|
|
|
|
"aperture": last_path_aperture})
|
|
|
|
current_x = x
|
|
|
|
current_y = y
|
|
|
|
path = [[current_x, current_y]] # Start new path
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Flash should not happen here
|
|
|
|
if current_operation_code == 3:
|
|
|
|
print "ERROR: Trying to flash within arc. (%d)" % line_num
|
|
|
|
continue
|
|
|
|
|
|
|
|
if quadrant_mode == 'MULTI':
|
|
|
|
center = [i + current_x, j + current_y]
|
|
|
|
radius = sqrt(i**2 + j**2)
|
|
|
|
start = arctan2(-j, -i)
|
|
|
|
stop = arctan2(-center[1] + y, -center[0] + x)
|
|
|
|
arcdir = [None, None, "cw", "ccw"]
|
|
|
|
this_arc = arc(center, radius, start, stop,
|
|
|
|
arcdir[current_interpolation_mode],
|
|
|
|
self.steps_per_circ)
|
|
|
|
|
|
|
|
# Last point in path is current point
|
|
|
|
current_x = this_arc[-1][0]
|
|
|
|
current_y = this_arc[-1][1]
|
|
|
|
|
|
|
|
# Append
|
|
|
|
path += this_arc
|
|
|
|
|
|
|
|
last_path_aperture = current_aperture
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
if quadrant_mode == 'SINGLE':
|
|
|
|
print "Warning: Single quadrant arc are not implemented yet. (%d)" % line_num
|
|
|
|
|
|
|
|
## G74/75* - Single or multiple quadrant arcs
|
|
|
|
match = self.quad_re.search(gline)
|
|
|
|
if match:
|
|
|
|
if match.group(1) == '4':
|
|
|
|
quadrant_mode = 'SINGLE'
|
|
|
|
else:
|
|
|
|
quadrant_mode = 'MULTI'
|
|
|
|
continue
|
|
|
|
|
|
|
|
## G37* - End region
|
2014-02-19 05:21:29 +00:00
|
|
|
if self.regionoff_re.search(gline):
|
2014-01-03 04:14:25 +00:00
|
|
|
# Only one path defines region?
|
2014-02-27 05:12:49 +00:00
|
|
|
if len(path) < 3:
|
|
|
|
print "ERROR: Path contains less than 3 points:"
|
|
|
|
print path
|
|
|
|
print "Line (%d): " % line_num, gline
|
|
|
|
path = []
|
|
|
|
continue
|
|
|
|
|
|
|
|
# For regions we may ignore an aperture that is None
|
2014-01-13 06:25:57 +00:00
|
|
|
self.regions.append({"polygon": Polygon(path),
|
|
|
|
"aperture": last_path_aperture})
|
2014-02-27 05:12:49 +00:00
|
|
|
#path = []
|
|
|
|
path = [[current_x, current_y]] # Start new path
|
2014-01-03 04:14:25 +00:00
|
|
|
continue
|
|
|
|
|
2014-01-13 06:25:57 +00:00
|
|
|
if gline.find("%ADD") != -1: # aperture definition
|
|
|
|
self.aperture_parse(gline) # adds element to apertures
|
2014-01-03 04:14:25 +00:00
|
|
|
continue
|
2014-02-19 05:21:29 +00:00
|
|
|
|
2014-02-27 05:12:49 +00:00
|
|
|
## G01/2/3* - Interpolation mode change
|
2014-02-19 05:21:29 +00:00
|
|
|
# Can occur along with coordinates and operation code but
|
|
|
|
# sometimes by itself (handled here).
|
|
|
|
# Example: G01*
|
|
|
|
match = self.interp_re.search(gline)
|
|
|
|
if match:
|
|
|
|
current_interpolation_mode = int(match.group(1))
|
2014-01-03 04:14:25 +00:00
|
|
|
continue
|
2014-02-19 05:21:29 +00:00
|
|
|
|
2014-02-27 05:12:49 +00:00
|
|
|
## Tool/aperture change
|
2014-02-19 05:21:29 +00:00
|
|
|
# Example: D12*
|
|
|
|
match = self.tool_re.search(gline)
|
|
|
|
if match:
|
|
|
|
current_aperture = match.group(1)
|
2014-01-03 04:14:25 +00:00
|
|
|
continue
|
2014-02-19 05:21:29 +00:00
|
|
|
|
2014-02-27 05:12:49 +00:00
|
|
|
## Number format
|
|
|
|
# Example: %FSLAX24Y24*%
|
2014-02-19 05:21:29 +00:00
|
|
|
# TODO: This is ignoring most of the format. Implement the rest.
|
|
|
|
match = self.fmt_re.search(gline)
|
|
|
|
if match:
|
|
|
|
self.int_digits = int(match.group(3))
|
|
|
|
self.frac_digits = int(match.group(4))
|
2014-01-04 05:47:57 +00:00
|
|
|
continue
|
2014-01-27 03:58:18 +00:00
|
|
|
|
2014-02-27 05:12:49 +00:00
|
|
|
## Mode (IN/MM)
|
|
|
|
# Example: %MOIN*%
|
2014-02-19 05:21:29 +00:00
|
|
|
match = self.mode_re.search(gline)
|
2014-01-27 03:58:18 +00:00
|
|
|
if match:
|
|
|
|
self.units = match.group(1)
|
|
|
|
continue
|
|
|
|
|
2014-02-27 05:12:49 +00:00
|
|
|
print "WARNING: Line ignored (%d):" % line_num, gline
|
2014-01-05 01:54:30 +00:00
|
|
|
|
|
|
|
if len(path) > 1:
|
2014-01-27 03:58:18 +00:00
|
|
|
# EOF, create shapely LineString if something still in path
|
2014-01-20 18:08:36 +00:00
|
|
|
self.paths.append({"linestring": LineString(path),
|
|
|
|
"aperture": last_path_aperture})
|
2014-02-19 05:21:29 +00:00
|
|
|
|
2014-01-07 03:08:55 +00:00
|
|
|
def do_flashes(self):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-08 06:46:50 +00:00
|
|
|
Creates geometry for Gerber flashes (aperture on a single point).
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-02-22 05:52:25 +00:00
|
|
|
|
2014-01-07 03:08:55 +00:00
|
|
|
self.flash_geometry = []
|
2014-01-03 04:14:25 +00:00
|
|
|
for flash in self.flashes:
|
2014-03-05 21:12:55 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
aperture = self.apertures[flash['aperture']]
|
|
|
|
except KeyError:
|
|
|
|
print "ERROR: Trying to flash with unknown aperture: ", flash['aperture']
|
|
|
|
continue
|
|
|
|
|
2014-01-13 06:25:57 +00:00
|
|
|
if aperture['type'] == 'C': # Circles
|
2014-01-03 04:14:25 +00:00
|
|
|
circle = Point(flash['loc']).buffer(aperture['size']/2)
|
2014-01-07 03:08:55 +00:00
|
|
|
self.flash_geometry.append(circle)
|
2014-01-03 04:14:25 +00:00
|
|
|
continue
|
2014-03-05 21:12:55 +00:00
|
|
|
|
2014-01-13 06:25:57 +00:00
|
|
|
if aperture['type'] == 'R': # Rectangles
|
2014-01-03 04:14:25 +00:00
|
|
|
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)
|
2014-01-07 03:08:55 +00:00
|
|
|
self.flash_geometry.append(rectangle)
|
2014-01-04 05:47:57 +00:00
|
|
|
continue
|
2014-03-05 21:12:55 +00:00
|
|
|
|
2014-02-12 02:50:03 +00:00
|
|
|
if aperture['type'] == 'O': # Obround
|
|
|
|
loc = flash['loc']
|
|
|
|
width = aperture['width']
|
|
|
|
height = aperture['height']
|
|
|
|
if width > height:
|
|
|
|
p1 = Point(loc[0] + 0.5*(width-height), loc[1])
|
|
|
|
p2 = Point(loc[0] - 0.5*(width-height), loc[1])
|
|
|
|
c1 = p1.buffer(height*0.5)
|
|
|
|
c2 = p2.buffer(height*0.5)
|
|
|
|
else:
|
|
|
|
p1 = Point(loc[0], loc[1] + 0.5*(height-width))
|
|
|
|
p2 = Point(loc[0], loc[1] - 0.5*(height-width))
|
|
|
|
c1 = p1.buffer(width*0.5)
|
|
|
|
c2 = p2.buffer(width*0.5)
|
|
|
|
obround = cascaded_union([c1, c2]).convex_hull
|
|
|
|
self.flash_geometry.append(obround)
|
|
|
|
continue
|
2014-03-05 21:12:55 +00:00
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
print "WARNING: Aperture type %s not implemented" % (aperture['type'])
|
2014-01-07 03:08:55 +00:00
|
|
|
|
|
|
|
def create_geometry(self):
|
2014-01-27 03:58:18 +00:00
|
|
|
"""
|
|
|
|
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.
|
2014-02-06 00:36:47 +00:00
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
:rtype : None
|
2014-02-06 00:36:47 +00:00
|
|
|
:return: None
|
2014-01-27 03:58:18 +00:00
|
|
|
"""
|
2014-02-22 05:52:25 +00:00
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
self.buffer_paths()
|
2014-03-04 03:12:07 +00:00
|
|
|
|
2014-01-07 03:08:55 +00:00
|
|
|
self.fix_regions()
|
2014-03-04 03:12:07 +00:00
|
|
|
|
2014-01-07 03:08:55 +00:00
|
|
|
self.do_flashes()
|
2014-03-04 03:12:07 +00:00
|
|
|
|
2014-02-22 05:52:25 +00:00
|
|
|
self.solid_geometry = cascaded_union(self.buffered_paths +
|
|
|
|
[poly['polygon'] for poly in self.regions] +
|
|
|
|
self.flash_geometry)
|
2014-01-03 04:14:25 +00:00
|
|
|
|
2014-02-06 00:36:47 +00:00
|
|
|
def get_bounding_box(self, margin=0.0, rounded=False):
|
|
|
|
"""
|
|
|
|
Creates and returns a rectangular polygon bounding at a distance of
|
|
|
|
margin from the object's ``solid_geometry``. If margin > 0, the polygon
|
|
|
|
can optionally have rounded corners of radius equal to margin.
|
|
|
|
|
|
|
|
:param margin: Distance to enlarge the rectangular bounding
|
2014-02-12 02:50:03 +00:00
|
|
|
box in both positive and negative, x and y axes.
|
2014-02-06 00:36:47 +00:00
|
|
|
:type margin: float
|
|
|
|
:param rounded: Wether or not to have rounded corners.
|
|
|
|
:type rounded: bool
|
|
|
|
:return: The bounding box.
|
|
|
|
:rtype: Shapely.Polygon
|
|
|
|
"""
|
2014-02-22 05:52:25 +00:00
|
|
|
|
2014-02-06 00:36:47 +00:00
|
|
|
bbox = self.solid_geometry.envelope.buffer(margin)
|
|
|
|
if not rounded:
|
|
|
|
bbox = bbox.envelope
|
|
|
|
return bbox
|
|
|
|
|
2014-01-13 06:25:57 +00:00
|
|
|
|
2014-01-08 06:46:50 +00:00
|
|
|
class Excellon(Geometry):
|
2014-01-27 03:58:18 +00:00
|
|
|
"""
|
|
|
|
*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``
|
|
|
|
================ ====================================
|
|
|
|
"""
|
2014-02-22 05:52:25 +00:00
|
|
|
|
2014-01-08 06:46:50 +00:00
|
|
|
def __init__(self):
|
2014-02-06 00:36:47 +00:00
|
|
|
"""
|
|
|
|
The constructor takes no parameters.
|
2014-02-27 05:12:49 +00:00
|
|
|
|
2014-02-06 00:36:47 +00:00
|
|
|
:return: Excellon object.
|
|
|
|
:rtype: Excellon
|
|
|
|
"""
|
2014-02-22 05:52:25 +00:00
|
|
|
|
2014-01-08 06:46:50 +00:00
|
|
|
Geometry.__init__(self)
|
|
|
|
|
|
|
|
self.tools = {}
|
|
|
|
|
|
|
|
self.drills = []
|
2014-01-27 03:58:18 +00:00
|
|
|
|
|
|
|
# Trailing "T" or leading "L"
|
|
|
|
self.zeros = ""
|
2014-01-31 05:39:34 +00:00
|
|
|
|
|
|
|
# Attributes to be included in serialization
|
|
|
|
# Always append to it because it carries contents
|
|
|
|
# from Geometry.
|
|
|
|
self.ser_attrs += ['tools', 'drills', 'zeros']
|
2014-02-27 05:12:49 +00:00
|
|
|
|
|
|
|
#### Patterns ####
|
|
|
|
# Regex basics:
|
|
|
|
# ^ - beginning
|
|
|
|
# $ - end
|
|
|
|
# *: 0 or more, +: 1 or more, ?: 0 or 1
|
|
|
|
|
|
|
|
# M48 - Beggining of Part Program Header
|
|
|
|
self.hbegin_re = re.compile(r'^M48$')
|
|
|
|
|
|
|
|
# M95 or % - End of Part Program Header
|
|
|
|
# NOTE: % has different meaning in the body
|
|
|
|
self.hend_re = re.compile(r'^(?:M95|%)$')
|
|
|
|
|
|
|
|
# FMAT Excellon format
|
|
|
|
self.fmat_re = re.compile(r'^FMAT,([12])$')
|
|
|
|
|
|
|
|
# Number format and units
|
|
|
|
# INCH uses 6 digits
|
|
|
|
# METRIC uses 5/6
|
|
|
|
self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?$')
|
|
|
|
|
|
|
|
# Tool definition/parameters (?= is look-ahead
|
|
|
|
# NOTE: This might be an overkill!
|
|
|
|
self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' +
|
|
|
|
r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
|
|
|
|
r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
|
|
|
|
r'(?=.*Z(-?\d*\.?\d*))?[CFSBHT]')
|
|
|
|
|
|
|
|
# Tool select
|
|
|
|
# Can have additional data after tool number but
|
|
|
|
# is ignored if present in the header.
|
|
|
|
# Warning: This will match toolset_re too.
|
|
|
|
self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))')
|
|
|
|
|
|
|
|
# Comment
|
|
|
|
self.comm_re = re.compile(r'^;(.*)$')
|
|
|
|
|
|
|
|
# Absolute/Incremental G90/G91
|
|
|
|
self.absinc_re = re.compile(r'^G9([01])$')
|
|
|
|
|
|
|
|
# Modes of operation
|
|
|
|
# 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill
|
|
|
|
self.modes_re = re.compile(r'^G0([012345])')
|
|
|
|
|
|
|
|
# Measuring mode
|
|
|
|
# 1-metric, 2-inch
|
|
|
|
self.meas_re = re.compile(r'^M7([12])$')
|
|
|
|
|
|
|
|
# Coordinates
|
2014-03-04 03:12:07 +00:00
|
|
|
#self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
|
|
|
|
#self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
|
|
|
|
self.coordsperiod_re = re.compile(r'(?=.*X(\d*\.\d*))?(?=.*Y(\d*\.\d*))?[XY]')
|
|
|
|
self.coordsnoperiod_re = re.compile(r'(?!.*\.)(?=.*X(\d*))?(?=.*Y(\d*))?[XY]')
|
2014-02-27 05:12:49 +00:00
|
|
|
|
|
|
|
# R - Repeat hole (# times, X offset, Y offset)
|
|
|
|
self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X(\d*\.?\d*))?(?:Y(\d*\.?\d*))?$')
|
|
|
|
|
|
|
|
# Various stop/pause commands
|
|
|
|
self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')
|
2014-01-08 06:46:50 +00:00
|
|
|
|
|
|
|
def parse_file(self, filename):
|
2014-02-27 05:12:49 +00:00
|
|
|
"""
|
|
|
|
Reads the specified file as array of lines as
|
|
|
|
passes it to ``parse_lines()``.
|
|
|
|
|
|
|
|
:param filename: The file to be read and parsed.
|
|
|
|
:type filename: str
|
|
|
|
:return: None
|
|
|
|
"""
|
2014-01-08 06:46:50 +00:00
|
|
|
efile = open(filename, 'r')
|
|
|
|
estr = efile.readlines()
|
|
|
|
efile.close()
|
|
|
|
self.parse_lines(estr)
|
|
|
|
|
|
|
|
def parse_lines(self, elines):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-08 06:46:50 +00:00
|
|
|
Main Excellon parser.
|
2014-02-27 05:12:49 +00:00
|
|
|
|
|
|
|
:param elines: List of strings, each being a line of Excellon code.
|
|
|
|
:type elines: list
|
|
|
|
:return: None
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-27 03:58:18 +00:00
|
|
|
|
2014-03-04 03:12:07 +00:00
|
|
|
# State variables
|
2014-01-08 06:46:50 +00:00
|
|
|
current_tool = ""
|
2014-02-27 05:12:49 +00:00
|
|
|
in_header = False
|
2014-03-04 03:12:07 +00:00
|
|
|
current_x = None
|
|
|
|
current_y = None
|
2014-02-27 05:12:49 +00:00
|
|
|
|
2014-03-04 03:12:07 +00:00
|
|
|
i = 0 # Line number
|
2014-01-08 06:46:50 +00:00
|
|
|
for eline in elines:
|
2014-03-04 03:12:07 +00:00
|
|
|
i += 1
|
2014-02-27 05:12:49 +00:00
|
|
|
|
|
|
|
## Header Begin/End ##
|
|
|
|
if self.hbegin_re.search(eline):
|
|
|
|
in_header = True
|
2014-01-08 06:46:50 +00:00
|
|
|
continue
|
2014-02-27 05:12:49 +00:00
|
|
|
|
|
|
|
if self.hend_re.search(eline):
|
|
|
|
in_header = False
|
2014-01-08 06:46:50 +00:00
|
|
|
continue
|
2014-01-27 03:58:18 +00:00
|
|
|
|
2014-02-27 05:12:49 +00:00
|
|
|
#### Body ####
|
|
|
|
if not in_header:
|
|
|
|
|
|
|
|
## Tool change ##
|
|
|
|
match = self.toolsel_re.search(eline)
|
|
|
|
if match:
|
|
|
|
current_tool = str(int(match.group(1)))
|
|
|
|
continue
|
|
|
|
|
2014-03-04 03:12:07 +00:00
|
|
|
## Coordinates without period ##
|
|
|
|
match = self.coordsnoperiod_re.search(eline)
|
|
|
|
if match:
|
|
|
|
try:
|
|
|
|
x = float(match.group(1))/10000
|
|
|
|
current_x = x
|
|
|
|
except TypeError:
|
|
|
|
x = current_x
|
|
|
|
|
|
|
|
try:
|
|
|
|
y = float(match.group(2))/10000
|
|
|
|
current_y = y
|
|
|
|
except TypeError:
|
|
|
|
y = current_y
|
|
|
|
|
|
|
|
if x is None or y is None:
|
|
|
|
print "ERROR: Missing coordinates"
|
|
|
|
continue
|
|
|
|
|
|
|
|
self.drills.append({'point': Point((x, y)), 'tool': current_tool})
|
|
|
|
continue
|
|
|
|
|
|
|
|
## Coordinates with period ##
|
|
|
|
match = self.coordsperiod_re.search(eline)
|
|
|
|
if match:
|
|
|
|
try:
|
|
|
|
x = float(match.group(1))
|
|
|
|
current_x = x
|
|
|
|
except TypeError:
|
|
|
|
x = current_x
|
|
|
|
|
|
|
|
try:
|
|
|
|
y = float(match.group(2))
|
2014-03-05 21:12:55 +00:00
|
|
|
current_y = y
|
2014-03-04 03:12:07 +00:00
|
|
|
except TypeError:
|
|
|
|
y = current_y
|
|
|
|
|
|
|
|
if x is None or y is None:
|
|
|
|
print "ERROR: Missing coordinates"
|
|
|
|
continue
|
|
|
|
|
2014-02-27 05:12:49 +00:00
|
|
|
self.drills.append({'point': Point((x, y)), 'tool': current_tool})
|
|
|
|
continue
|
|
|
|
|
|
|
|
#### Header ####
|
|
|
|
if in_header:
|
|
|
|
|
|
|
|
## Tool definitions ##
|
|
|
|
match = self.toolset_re.search(eline)
|
|
|
|
if match:
|
|
|
|
name = str(int(match.group(1)))
|
|
|
|
spec = {
|
|
|
|
"C": float(match.group(2)),
|
|
|
|
# "F": float(match.group(3)),
|
|
|
|
# "S": float(match.group(4)),
|
|
|
|
# "B": float(match.group(5)),
|
|
|
|
# "H": float(match.group(6)),
|
|
|
|
# "Z": float(match.group(7))
|
|
|
|
}
|
|
|
|
self.tools[name] = spec
|
|
|
|
continue
|
|
|
|
|
|
|
|
## Units and number format ##
|
|
|
|
match = self.units_re.match(eline)
|
|
|
|
if match:
|
|
|
|
self.zeros = match.group(2) # "T" or "L"
|
|
|
|
self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
|
|
|
|
continue
|
2014-01-27 03:58:18 +00:00
|
|
|
|
2014-01-08 06:46:50 +00:00
|
|
|
print "WARNING: Line ignored:", eline
|
|
|
|
|
|
|
|
def create_geometry(self):
|
|
|
|
self.solid_geometry = []
|
2014-02-27 05:12:49 +00:00
|
|
|
|
2014-01-08 06:46:50 +00:00
|
|
|
for drill in self.drills:
|
2014-02-27 05:12:49 +00:00
|
|
|
poly = Point(drill['point']).buffer(self.tools[drill['tool']]["C"]/2.0)
|
2014-01-08 06:46:50 +00:00
|
|
|
self.solid_geometry.append(poly)
|
2014-02-27 05:12:49 +00:00
|
|
|
|
|
|
|
#self.solid_geometry = cascaded_union(self.solid_geometry)
|
2014-01-08 06:46:50 +00:00
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
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.
|
2014-02-01 19:23:31 +00:00
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
:param factor: Number by which to scale the object.
|
|
|
|
:type factor: float
|
|
|
|
:return: None
|
|
|
|
:rtype: NOne
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Drills
|
|
|
|
for drill in self.drills:
|
2014-02-01 19:23:31 +00:00
|
|
|
drill['point'] = affinity.scale(drill['point'], factor, factor, origin=(0, 0))
|
2014-01-27 03:58:18 +00:00
|
|
|
|
2014-02-22 05:52:25 +00:00
|
|
|
self.create_geometry()
|
|
|
|
|
|
|
|
def offset(self, vect):
|
|
|
|
"""
|
|
|
|
Offsets geometry on the XY plane in the object by a given vector.
|
|
|
|
|
|
|
|
:param vect: (x, y) offset vector.
|
|
|
|
:type vect: tuple
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
|
|
|
|
dx, dy = vect
|
|
|
|
|
|
|
|
# Drills
|
|
|
|
for drill in self.drills:
|
|
|
|
drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy)
|
|
|
|
|
|
|
|
self.create_geometry()
|
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
def convert_units(self, units):
|
|
|
|
factor = Geometry.convert_units(self, units)
|
|
|
|
|
|
|
|
# Tools
|
|
|
|
for tname in self.tools:
|
2014-02-27 05:12:49 +00:00
|
|
|
self.tools[tname]["C"] *= factor
|
|
|
|
|
|
|
|
self.create_geometry()
|
2014-01-27 03:58:18 +00:00
|
|
|
|
|
|
|
return factor
|
|
|
|
|
2014-01-13 06:25:57 +00:00
|
|
|
|
2014-01-10 03:14:46 +00:00
|
|
|
class CNCjob(Geometry):
|
2014-01-27 03:58:18 +00:00
|
|
|
"""
|
|
|
|
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).
|
|
|
|
===================== =========================================
|
|
|
|
"""
|
2014-01-13 06:25:57 +00:00
|
|
|
def __init__(self, units="in", kind="generic", z_move=0.1,
|
|
|
|
feedrate=3.0, z_cut=-0.002, tooldia=0.0):
|
2014-01-20 18:08:36 +00:00
|
|
|
|
|
|
|
Geometry.__init__(self)
|
2014-01-04 05:47:57 +00:00
|
|
|
self.kind = kind
|
|
|
|
self.units = units
|
|
|
|
self.z_cut = z_cut
|
|
|
|
self.z_move = z_move
|
|
|
|
self.feedrate = feedrate
|
2014-01-13 03:07:24 +00:00
|
|
|
self.tooldia = tooldia
|
2014-01-27 03:58:18 +00:00
|
|
|
self.unitcode = {"IN": "G20", "MM": "G21"}
|
2014-01-04 05:47:57 +00:00
|
|
|
self.pausecode = "G04 P1"
|
|
|
|
self.feedminutecode = "G94"
|
|
|
|
self.absolutecode = "G90"
|
|
|
|
self.gcode = ""
|
2014-01-05 01:54:30 +00:00
|
|
|
self.input_geometry_bounds = None
|
2014-01-10 03:14:46 +00:00
|
|
|
self.gcode_parsed = None
|
2014-01-27 03:58:18 +00:00
|
|
|
self.steps_per_circ = 20 # Used when parsing G-code arcs
|
2014-01-31 05:39:34 +00:00
|
|
|
|
|
|
|
# Attributes to be included in serialization
|
|
|
|
# Always append to it because it carries contents
|
|
|
|
# from Geometry.
|
|
|
|
self.ser_attrs += ['kind', 'z_cut', 'z_move', 'feedrate', 'tooldia',
|
|
|
|
'gcode', 'input_geometry_bounds', 'gcode_parsed',
|
|
|
|
'steps_per_circ']
|
2014-02-01 19:23:31 +00:00
|
|
|
|
|
|
|
def convert_units(self, units):
|
|
|
|
factor = Geometry.convert_units(self, units)
|
|
|
|
print "CNCjob.convert_units()"
|
|
|
|
|
|
|
|
self.z_cut *= factor
|
|
|
|
self.z_move *= factor
|
|
|
|
self.feedrate *= factor
|
|
|
|
self.tooldia *= factor
|
|
|
|
|
|
|
|
return factor
|
|
|
|
|
2014-01-05 21:27:57 +00:00
|
|
|
def generate_from_excellon(self, exobj):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-27 03:58:18 +00:00
|
|
|
Generates G-code for drilling from Excellon object.
|
2014-01-05 21:27:57 +00:00
|
|
|
self.gcode becomes a list, each element is a
|
|
|
|
different job for each tool in the excellon code.
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-05 21:27:57 +00:00
|
|
|
self.kind = "drill"
|
|
|
|
self.gcode = []
|
|
|
|
|
|
|
|
t = "G00 X%.4fY%.4f\n"
|
2014-01-20 18:08:36 +00:00
|
|
|
down = "G01 Z%.4f\n" % self.z_cut
|
|
|
|
up = "G01 Z%.4f\n" % self.z_move
|
|
|
|
|
2014-01-05 21:27:57 +00:00
|
|
|
for tool in exobj.tools:
|
|
|
|
|
|
|
|
points = []
|
|
|
|
|
|
|
|
for drill in exobj.drill:
|
|
|
|
if drill['tool'] == tool:
|
|
|
|
points.append(drill['point'])
|
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
gcode = self.unitcode[self.units.upper()] + "\n"
|
2014-01-05 21:27:57 +00:00
|
|
|
gcode += self.absolutecode + "\n"
|
|
|
|
gcode += self.feedminutecode + "\n"
|
2014-01-20 18:08:36 +00:00
|
|
|
gcode += "F%.2f\n" % self.feedrate
|
|
|
|
gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
|
|
|
|
gcode += "M03\n" # Spindle start
|
2014-01-05 21:27:57 +00:00
|
|
|
gcode += self.pausecode + "\n"
|
|
|
|
|
|
|
|
for point in points:
|
2014-01-20 18:08:36 +00:00
|
|
|
gcode += t % point
|
2014-01-05 21:27:57 +00:00
|
|
|
gcode += down + up
|
|
|
|
|
2014-01-20 18:08:36 +00:00
|
|
|
gcode += t % (0, 0)
|
2014-01-13 06:25:57 +00:00
|
|
|
gcode += "M05\n" # Spindle stop
|
2014-01-05 21:27:57 +00:00
|
|
|
|
|
|
|
self.gcode.append(gcode)
|
2014-01-20 18:08:36 +00:00
|
|
|
|
|
|
|
def generate_from_excellon_by_tool(self, exobj, tools="all"):
|
|
|
|
"""
|
|
|
|
Creates gcode for this object from an Excellon object
|
|
|
|
for the specified tools.
|
2014-02-21 04:26:57 +00:00
|
|
|
|
|
|
|
:param exobj: Excellon object to process
|
|
|
|
:type exobj: Excellon
|
|
|
|
:param tools: Comma separated tool names
|
|
|
|
:type: tools: str
|
|
|
|
:return: None
|
|
|
|
:rtype: None
|
2014-01-20 18:08:36 +00:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
gcode = self.unitcode[self.units.upper()] + "\n"
|
2014-01-20 18:08:36 +00:00
|
|
|
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
|
|
|
|
|
2014-02-21 04:26:57 +00:00
|
|
|
def generate_from_geometry(self, geometry, append=True, tooldia=None, tolerance=0):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-02-21 04:26:57 +00:00
|
|
|
Generates G-Code from a Geometry object. Stores in ``self.gcode``.
|
|
|
|
|
|
|
|
:param geometry: Geometry defining the toolpath
|
|
|
|
:type geometry: Geometry
|
|
|
|
:param append: Wether to append to self.gcode or re-write it.
|
|
|
|
:type append: bool
|
|
|
|
:param tooldia: If given, sets the tooldia property but does
|
2014-03-01 00:32:04 +00:00
|
|
|
not affect the process in any other way.
|
2014-02-21 04:26:57 +00:00
|
|
|
:type tooldia: bool
|
|
|
|
:param tolerance: All points in the simplified object will be within the
|
2014-03-01 00:32:04 +00:00
|
|
|
tolerance distance of the original geometry.
|
2014-02-21 04:26:57 +00:00
|
|
|
:return: None
|
|
|
|
:rtype: None
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-20 18:08:36 +00:00
|
|
|
if tooldia is not None:
|
2014-01-05 01:54:30 +00:00
|
|
|
self.tooldia = tooldia
|
|
|
|
|
2014-01-20 18:08:36 +00:00
|
|
|
self.input_geometry_bounds = geometry.bounds()
|
2014-01-05 01:54:30 +00:00
|
|
|
|
2014-01-13 06:25:57 +00:00
|
|
|
if not append:
|
2014-01-04 05:47:57 +00:00
|
|
|
self.gcode = ""
|
2014-01-20 18:08:36 +00:00
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
self.gcode = self.unitcode[self.units.upper()] + "\n"
|
2014-01-04 05:47:57 +00:00
|
|
|
self.gcode += self.absolutecode + "\n"
|
|
|
|
self.gcode += self.feedminutecode + "\n"
|
2014-01-20 18:08:36 +00:00
|
|
|
self.gcode += "F%.2f\n" % self.feedrate
|
|
|
|
self.gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
|
2014-01-14 05:13:09 +00:00
|
|
|
self.gcode += "M03\n" # Spindle start
|
2014-01-04 05:47:57 +00:00
|
|
|
self.gcode += self.pausecode + "\n"
|
2014-01-05 01:54:30 +00:00
|
|
|
|
2014-01-20 18:08:36 +00:00
|
|
|
for geo in geometry.solid_geometry:
|
2014-01-04 05:47:57 +00:00
|
|
|
|
|
|
|
if type(geo) == Polygon:
|
2014-02-21 04:26:57 +00:00
|
|
|
self.gcode += self.polygon2gcode(geo, tolerance=tolerance)
|
2014-01-04 05:47:57 +00:00
|
|
|
continue
|
|
|
|
|
2014-01-10 03:14:46 +00:00
|
|
|
if type(geo) == LineString or type(geo) == LinearRing:
|
2014-02-21 04:26:57 +00:00
|
|
|
self.gcode += self.linear2gcode(geo, tolerance=tolerance)
|
2014-01-04 05:47:57 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
if type(geo) == Point:
|
2014-01-20 18:08:36 +00:00
|
|
|
self.gcode += self.point2gcode(geo)
|
2014-01-04 05:47:57 +00:00
|
|
|
continue
|
2014-01-20 18:08:36 +00:00
|
|
|
|
|
|
|
if type(geo) == MultiPolygon:
|
|
|
|
for poly in geo:
|
2014-02-21 04:26:57 +00:00
|
|
|
self.gcode += self.polygon2gcode(poly, tolerance=tolerance)
|
2014-01-20 18:08:36 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
print "WARNING: G-code generation not implemented for %s" % (str(type(geo)))
|
2014-01-04 05:47:57 +00:00
|
|
|
|
2014-01-20 18:08:36 +00:00
|
|
|
self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
2014-01-05 21:27:57 +00:00
|
|
|
self.gcode += "G00 X0Y0\n"
|
2014-01-13 06:25:57 +00:00
|
|
|
self.gcode += "M05\n" # Spindle stop
|
2014-01-27 03:58:18 +00:00
|
|
|
|
|
|
|
def pre_parse(self, gtext):
|
|
|
|
"""
|
2014-02-21 04:26:57 +00:00
|
|
|
Separates parts of the G-Code text into a list of dictionaries.
|
|
|
|
Used by ``self.gcode_parse()``.
|
|
|
|
|
|
|
|
:param gtext: A single string with g-code
|
2014-01-27 03:58:18 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
2014-01-10 03:14:46 +00:00
|
|
|
def gcode_parse(self):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-15 04:43:04 +00:00
|
|
|
G-Code parser (from self.gcode). Generates dictionary with
|
|
|
|
single-segment LineString's and "kind" indicating cut or travel,
|
2014-01-05 21:27:57 +00:00
|
|
|
fast or feedrate speed.
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-15 04:43:04 +00:00
|
|
|
|
2014-02-22 05:52:25 +00:00
|
|
|
kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
|
|
|
|
|
2014-01-20 18:08:36 +00:00
|
|
|
# Results go here
|
2014-01-04 05:47:57 +00:00
|
|
|
geometry = []
|
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
# TODO: Merge into single parser?
|
|
|
|
gobjs = self.pre_parse(self.gcode)
|
2014-01-04 05:47:57 +00:00
|
|
|
|
|
|
|
# Last known instruction
|
|
|
|
current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
|
2014-02-22 05:52:25 +00:00
|
|
|
|
|
|
|
# Current path: temporary storage until tool is
|
|
|
|
# lifted or lowered.
|
|
|
|
path = []
|
|
|
|
|
2014-01-04 05:47:57 +00:00
|
|
|
# Process every instruction
|
|
|
|
for gobj in gobjs:
|
2014-02-22 05:52:25 +00:00
|
|
|
|
|
|
|
# Changing height:
|
2014-01-04 05:47:57 +00:00
|
|
|
if 'Z' in gobj:
|
|
|
|
if ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']:
|
2014-01-05 01:54:30 +00:00
|
|
|
print "WARNING: Non-orthogonal motion: From", current
|
2014-01-04 05:47:57 +00:00
|
|
|
print " To:", gobj
|
|
|
|
current['Z'] = gobj['Z']
|
2014-02-22 05:52:25 +00:00
|
|
|
# Store the path into geometry and reset path
|
|
|
|
if len(path) > 1:
|
|
|
|
geometry.append({"geom": LineString(path),
|
|
|
|
"kind": kind})
|
|
|
|
path = [path[-1]] # Start with the last point of last path.
|
|
|
|
|
|
|
|
|
2014-01-04 05:47:57 +00:00
|
|
|
if 'G' in gobj:
|
2014-01-08 06:46:50 +00:00
|
|
|
current['G'] = int(gobj['G'])
|
2014-01-04 05:47:57 +00:00
|
|
|
|
|
|
|
if 'X' in gobj or 'Y' in gobj:
|
2014-01-08 06:46:50 +00:00
|
|
|
|
2014-01-04 05:47:57 +00:00
|
|
|
if 'X' in gobj:
|
|
|
|
x = gobj['X']
|
|
|
|
else:
|
|
|
|
x = current['X']
|
2014-01-08 06:46:50 +00:00
|
|
|
|
2014-01-04 05:47:57 +00:00
|
|
|
if 'Y' in gobj:
|
|
|
|
y = gobj['Y']
|
|
|
|
else:
|
|
|
|
y = current['Y']
|
2014-02-22 05:52:25 +00:00
|
|
|
|
|
|
|
kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
|
|
|
|
|
2014-01-04 05:47:57 +00:00
|
|
|
if current['Z'] > 0:
|
|
|
|
kind[0] = 'T'
|
2014-01-08 06:46:50 +00:00
|
|
|
if current['G'] > 0:
|
2014-01-04 05:47:57 +00:00
|
|
|
kind[1] = 'S'
|
2014-01-08 06:46:50 +00:00
|
|
|
|
|
|
|
arcdir = [None, None, "cw", "ccw"]
|
2014-01-27 03:58:18 +00:00
|
|
|
if current['G'] in [0, 1]: # line
|
2014-02-22 05:52:25 +00:00
|
|
|
path.append((x, y))
|
2014-02-27 05:12:49 +00:00
|
|
|
|
2014-01-13 06:25:57 +00:00
|
|
|
if current['G'] in [2, 3]: # arc
|
2014-01-08 06:46:50 +00:00
|
|
|
center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
|
|
|
|
radius = sqrt(gobj['I']**2 + gobj['J']**2)
|
2014-01-20 18:08:36 +00:00
|
|
|
start = arctan2(-gobj['J'], -gobj['I'])
|
|
|
|
stop = arctan2(-center[1]+y, -center[0]+x)
|
2014-02-22 05:52:25 +00:00
|
|
|
path += arc(center, radius, start, stop,
|
|
|
|
arcdir[current['G']],
|
|
|
|
self.steps_per_circ)
|
2014-01-13 06:25:57 +00:00
|
|
|
|
2014-01-04 05:47:57 +00:00
|
|
|
# Update current instruction
|
|
|
|
for code in gobj:
|
|
|
|
current[code] = gobj[code]
|
2014-02-22 05:52:25 +00:00
|
|
|
|
2014-01-10 03:14:46 +00:00
|
|
|
self.gcode_parsed = geometry
|
2014-01-04 05:47:57 +00:00
|
|
|
return geometry
|
2014-01-05 01:54:30 +00:00
|
|
|
|
2014-02-01 19:23:31 +00:00
|
|
|
# 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
|
2014-01-05 21:27:57 +00:00
|
|
|
|
2014-01-08 06:46:50 +00:00
|
|
|
def plot2(self, axes, tooldia=None, dpi=75, margin=0.1,
|
2014-01-13 06:25:57 +00:00
|
|
|
color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
|
2014-02-22 05:52:25 +00:00
|
|
|
alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-10 03:14:46 +00:00
|
|
|
Plots the G-code job onto the given axes.
|
2014-02-21 04:26:57 +00:00
|
|
|
|
|
|
|
:param axes: Matplotlib axes on which to plot.
|
|
|
|
:param tooldia: Tool diameter.
|
|
|
|
:param dpi: Not used!
|
|
|
|
:param margin: Not used!
|
|
|
|
:param color: Color specification.
|
|
|
|
:param alpha: Transparency specification.
|
|
|
|
:param tool_tolerance: Tolerance when drawing the toolshape.
|
|
|
|
:return: None
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
|
|
|
if tooldia is None:
|
2014-01-08 06:46:50 +00:00
|
|
|
tooldia = self.tooldia
|
2014-01-03 04:14:25 +00:00
|
|
|
|
2014-01-08 06:46:50 +00:00
|
|
|
if tooldia == 0:
|
2014-01-10 03:14:46 +00:00
|
|
|
for geo in self.gcode_parsed:
|
2014-01-08 06:46:50 +00:00
|
|
|
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:
|
2014-01-10 03:14:46 +00:00
|
|
|
for geo in self.gcode_parsed:
|
2014-02-21 04:26:57 +00:00
|
|
|
poly = geo['geom'].buffer(tooldia/2.0).simplify(tool_tolerance)
|
2014-01-08 06:46:50 +00:00
|
|
|
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)
|
2014-01-03 04:14:25 +00:00
|
|
|
|
2014-01-10 03:14:46 +00:00
|
|
|
def create_geometry(self):
|
2014-02-21 04:26:57 +00:00
|
|
|
# TODO: This takes forever. Too much data?
|
2014-01-10 03:14:46 +00:00
|
|
|
self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
|
|
|
|
|
2014-02-21 04:26:57 +00:00
|
|
|
def polygon2gcode(self, polygon, tolerance=0):
|
2014-01-20 18:08:36 +00:00
|
|
|
"""
|
|
|
|
Creates G-Code for the exterior and all interior paths
|
|
|
|
of a polygon.
|
2014-02-01 19:23:31 +00:00
|
|
|
|
|
|
|
:param polygon: A Shapely.Polygon
|
|
|
|
:type polygon: Shapely.Polygon
|
2014-02-21 04:26:57 +00:00
|
|
|
:param tolerance: All points in the simplified object will be within the
|
2014-03-01 00:32:04 +00:00
|
|
|
tolerance distance of the original geometry.
|
2014-02-21 04:26:57 +00:00
|
|
|
:type tolerance: float
|
|
|
|
:return: G-code to cut along polygon.
|
|
|
|
:rtype: str
|
2014-01-20 18:08:36 +00:00
|
|
|
"""
|
2014-02-21 04:26:57 +00:00
|
|
|
|
|
|
|
if tolerance > 0:
|
|
|
|
target_polygon = polygon.simplify(tolerance)
|
|
|
|
else:
|
|
|
|
target_polygon = polygon
|
|
|
|
|
2014-01-20 18:08:36 +00:00
|
|
|
gcode = ""
|
|
|
|
t = "G0%d X%.4fY%.4f\n"
|
2014-02-21 04:26:57 +00:00
|
|
|
path = list(target_polygon.exterior.coords) # Polygon exterior
|
2014-01-20 18:08:36 +00:00
|
|
|
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
|
2014-02-21 04:26:57 +00:00
|
|
|
for ints in target_polygon.interiors: # Polygon interiors
|
2014-01-20 18:08:36 +00:00
|
|
|
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
|
|
|
|
|
2014-02-21 04:26:57 +00:00
|
|
|
def linear2gcode(self, linear, tolerance=0):
|
|
|
|
"""
|
|
|
|
Generates G-code to cut along the linear feature.
|
|
|
|
|
|
|
|
:param linear: The path to cut along.
|
|
|
|
:type: Shapely.LinearRing or Shapely.Linear String
|
|
|
|
:param tolerance: All points in the simplified object will be within the
|
2014-03-01 00:32:04 +00:00
|
|
|
tolerance distance of the original geometry.
|
2014-02-21 04:26:57 +00:00
|
|
|
:type tolerance: float
|
|
|
|
:return: G-code to cut alon the linear feature.
|
|
|
|
:rtype: str
|
|
|
|
"""
|
|
|
|
|
|
|
|
if tolerance > 0:
|
|
|
|
target_linear = linear.simplify(tolerance)
|
|
|
|
else:
|
|
|
|
target_linear = linear
|
|
|
|
|
2014-01-20 18:08:36 +00:00
|
|
|
gcode = ""
|
|
|
|
t = "G0%d X%.4fY%.4f\n"
|
2014-02-21 04:26:57 +00:00
|
|
|
path = list(target_linear.coords)
|
|
|
|
gcode += t % (0, path[0][0], path[0][1]) # Move to first point
|
2014-01-20 18:08:36 +00:00
|
|
|
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):
|
2014-01-31 05:39:34 +00:00
|
|
|
# TODO: This is not doing anything.
|
2014-01-20 18:08:36 +00:00
|
|
|
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
|
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
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.
|
2014-02-01 19:23:31 +00:00
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
:param factor: Number by which to scale the object.
|
|
|
|
:type factor: float
|
|
|
|
:return: None
|
|
|
|
:rtype: None
|
|
|
|
"""
|
2014-02-01 19:23:31 +00:00
|
|
|
|
2014-01-27 03:58:18 +00:00
|
|
|
for g in self.gcode_parsed:
|
|
|
|
g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
|
2014-01-03 04:14:25 +00:00
|
|
|
|
2014-02-01 19:23:31 +00:00
|
|
|
self.create_geometry()
|
2014-01-08 06:46:50 +00:00
|
|
|
|
2014-02-22 05:52:25 +00:00
|
|
|
def offset(self, vect):
|
|
|
|
"""
|
|
|
|
Offsets all the geometry on the XY plane in the object by the
|
|
|
|
given vector.
|
|
|
|
|
|
|
|
:param vect: (x, y) offset vector.
|
|
|
|
:type vect: tuple
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
dx, dy = vect
|
|
|
|
|
|
|
|
for g in self.gcode_parsed:
|
|
|
|
g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
|
|
|
|
|
|
|
|
self.create_geometry()
|
2014-01-13 06:25:57 +00:00
|
|
|
|
2014-01-10 03:14:46 +00:00
|
|
|
def get_bounds(geometry_set):
|
2014-01-08 06:46:50 +00:00
|
|
|
xmin = Inf
|
|
|
|
ymin = Inf
|
|
|
|
xmax = -Inf
|
|
|
|
ymax = -Inf
|
2014-01-20 18:08:36 +00:00
|
|
|
|
2014-03-05 21:12:55 +00:00
|
|
|
#print "Getting bounds of:", str(geometry_set)
|
2014-01-10 03:14:46 +00:00
|
|
|
for gs in geometry_set:
|
2014-02-06 00:36:47 +00:00
|
|
|
try:
|
|
|
|
gxmin, gymin, gxmax, gymax = geometry_set[gs].bounds()
|
|
|
|
xmin = min([xmin, gxmin])
|
|
|
|
ymin = min([ymin, gymin])
|
|
|
|
xmax = max([xmax, gxmax])
|
|
|
|
ymax = max([ymax, gymax])
|
|
|
|
except:
|
|
|
|
print "DEV WARNING: Tried to get bounds of empty geometry."
|
|
|
|
|
2014-01-08 06:46:50 +00:00
|
|
|
return [xmin, ymin, xmax, ymax]
|
2014-01-03 04:14:25 +00:00
|
|
|
|
2014-01-13 06:25:57 +00:00
|
|
|
|
2014-01-08 06:46:50 +00:00
|
|
|
def arc(center, radius, start, stop, direction, steps_per_circ):
|
2014-01-18 07:01:34 +00:00
|
|
|
"""
|
2014-02-22 05:52:25 +00:00
|
|
|
Creates a list of point along the specified arc.
|
2014-02-19 05:21:29 +00:00
|
|
|
|
|
|
|
: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
|
2014-01-18 07:01:34 +00:00
|
|
|
represent a circle.
|
2014-02-19 05:21:29 +00:00
|
|
|
:type steps_per_circ: int
|
2014-02-22 05:52:25 +00:00
|
|
|
:return: The desired arc, as list of tuples
|
|
|
|
:rtype: list
|
2014-01-18 07:01:34 +00:00
|
|
|
"""
|
2014-02-22 05:52:25 +00:00
|
|
|
# TODO: Resolution should be established by fraction of total length, not angle.
|
|
|
|
|
2014-01-13 06:25:57 +00:00
|
|
|
da_sign = {"cw": -1.0, "ccw": 1.0}
|
2014-01-08 06:46:50 +00:00
|
|
|
points = []
|
2014-01-13 06:25:57 +00:00
|
|
|
if direction == "ccw" and stop <= start:
|
2014-01-08 06:46:50 +00:00
|
|
|
stop += 2*pi
|
2014-01-13 06:25:57 +00:00
|
|
|
if direction == "cw" and stop >= start:
|
2014-01-08 06:46:50 +00:00
|
|
|
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
|
2014-02-22 05:52:25 +00:00
|
|
|
points.append((center[0]+radius*cos(theta), center[1]+radius*sin(theta)))
|
|
|
|
return points
|
2014-01-13 06:25:57 +00:00
|
|
|
|
|
|
|
|
2014-01-20 18:08:36 +00:00
|
|
|
def clear_poly(poly, tooldia, overlap=0.1):
|
2014-01-18 07:01:34 +00:00
|
|
|
"""
|
|
|
|
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.
|
2014-02-19 05:21:29 +00:00
|
|
|
|
|
|
|
: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
|
2014-01-18 07:01:34 +00:00
|
|
|
in each pass.
|
2014-02-19 05:21:29 +00:00
|
|
|
:type overlap: float
|
|
|
|
:return: list of Shapely.Polygon
|
|
|
|
:rtype: list
|
2014-01-18 07:01:34 +00:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
|
2014-02-06 00:36:47 +00:00
|
|
|
|
2014-01-31 05:39:34 +00:00
|
|
|
def to_dict(geo):
|
|
|
|
output = ''
|
|
|
|
if isinstance(geo, BaseGeometry):
|
|
|
|
return {
|
|
|
|
"__class__": "Shply",
|
|
|
|
"__inst__": sdumps(geo)
|
|
|
|
}
|
|
|
|
return geo
|
|
|
|
|
2014-02-06 00:36:47 +00:00
|
|
|
|
2014-01-31 05:39:34 +00:00
|
|
|
def dict2obj(d):
|
|
|
|
if '__class__' in d and '__inst__' in d:
|
|
|
|
# For now assume all classes are Shapely geometry.
|
|
|
|
return sloads(d['__inst__'])
|
|
|
|
else:
|
|
|
|
return d
|
|
|
|
|
2014-02-06 00:36:47 +00:00
|
|
|
|
|
|
|
def plotg(geo):
|
|
|
|
try:
|
|
|
|
_ = iter(geo)
|
|
|
|
except:
|
|
|
|
geo = [geo]
|
|
|
|
|
|
|
|
for g in geo:
|
|
|
|
if type(g) == Polygon:
|
|
|
|
x, y = g.exterior.coords.xy
|
|
|
|
plot(x, y)
|
|
|
|
for ints in g.interiors:
|
|
|
|
x, y = ints.coords.xy
|
|
|
|
plot(x, y)
|
|
|
|
continue
|
|
|
|
|
|
|
|
if type(g) == LineString or type(g) == LinearRing:
|
|
|
|
x, y = g.coords.xy
|
|
|
|
plot(x, y)
|
|
|
|
continue
|
|
|
|
|
|
|
|
if type(g) == Point:
|
|
|
|
x, y = g.coords.xy
|
|
|
|
plot(x, y, 'o')
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
_ = iter(g)
|
|
|
|
plotg(g)
|
|
|
|
except:
|
|
|
|
print "Cannot plot:", str(type(g))
|
|
|
|
continue
|
|
|
|
|
2014-02-19 05:21:29 +00:00
|
|
|
def parse_gerber_number(strnumber, frac_digits):
|
|
|
|
"""
|
|
|
|
Parse a single number of Gerber coordinates.
|
|
|
|
|
|
|
|
:param strnumber: String containing a number in decimal digits
|
|
|
|
from a coordinate data block, possibly with a leading sign.
|
|
|
|
:type strnumber: str
|
|
|
|
:param frac_digits: Number of digits used for the fractional
|
|
|
|
part of the number
|
|
|
|
:type frac_digits: int
|
|
|
|
:return: The number in floating point.
|
|
|
|
:rtype: float
|
|
|
|
"""
|
|
|
|
return int(strnumber)*(10**(-frac_digits))
|
2014-02-06 00:36:47 +00:00
|
|
|
|
2014-02-19 05:21:29 +00:00
|
|
|
def parse_gerber_coords(gstr, int_digits, frac_digits):
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-03 04:14:25 +00:00
|
|
|
Parse Gerber coordinates
|
2014-02-19 05:21:29 +00:00
|
|
|
|
|
|
|
:param gstr: Line of G-Code containing coordinates.
|
|
|
|
:type gstr: str
|
|
|
|
:param int_digits: Number of digits in integer part of a number.
|
|
|
|
:type int_digits: int
|
|
|
|
:param frac_digits: Number of digits in frac_digits part of a number.
|
|
|
|
:type frac_digits: int
|
|
|
|
:return: [x, y] coordinates.
|
|
|
|
:rtype: list
|
2014-01-13 06:25:57 +00:00
|
|
|
"""
|
2014-01-03 04:14:25 +00:00
|
|
|
global gerbx, gerby
|
|
|
|
xindex = gstr.find("X")
|
|
|
|
yindex = gstr.find("Y")
|
|
|
|
index = gstr.find("D")
|
2014-01-13 06:25:57 +00:00
|
|
|
if xindex == -1:
|
2014-01-03 04:14:25 +00:00
|
|
|
x = gerbx
|
2014-02-19 05:21:29 +00:00
|
|
|
y = int(gstr[(yindex+1):index])*(10**(-frac_digits))
|
2014-01-13 06:25:57 +00:00
|
|
|
elif yindex == -1:
|
2014-01-03 04:14:25 +00:00
|
|
|
y = gerby
|
2014-02-19 05:21:29 +00:00
|
|
|
x = int(gstr[(xindex+1):index])*(10**(-frac_digits))
|
2014-01-03 04:14:25 +00:00
|
|
|
else:
|
2014-02-19 05:21:29 +00:00
|
|
|
x = int(gstr[(xindex+1):yindex])*(10**(-frac_digits))
|
|
|
|
y = int(gstr[(yindex+1):index])*(10**(-frac_digits))
|
2014-01-03 04:14:25 +00:00
|
|
|
gerbx = x
|
|
|
|
gerby = y
|
2014-01-13 06:25:57 +00:00
|
|
|
return [x, y]
|