Offset feature. G-Code/CNC job processing fixes and optimization.

This commit is contained in:
Juan Pablo Caram
2014-02-22 00:52:25 -05:00
parent f8352e7188
commit 42f3652668
10 changed files with 799 additions and 98 deletions

166
camlib.py
View File

@@ -43,6 +43,11 @@ class Geometry:
"""
Creates contours around geometry at a given
offset distance.
:param offset: Offset distance.
:type offset: float
:return: The buffered geometry.
:rtype: Shapely.MultiPolygon or Shapely.Polygon
"""
return self.solid_geometry.buffer(offset)
@@ -107,6 +112,16 @@ class Geometry:
"""
return
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
def convert_units(self, units):
"""
Converts the units of the object to ``units`` by scaling all
@@ -379,16 +394,63 @@ class Gerber (Geometry):
# Now buffered_paths, flash_geometry and solid_geometry
self.create_geometry()
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
print "Shifting paths..."
for path in self.paths:
path['linestring'] = affinity.translate(path['linestring'],
xoff=dx, yoff=dy)
# Flashes
print "Shifting flashes..."
for fl in self.flashes:
# TODO: Shouldn't 'loc' be a numpy.array()?
fl['loc'][0] += dx
fl['loc'][1] += dy
# Regions
print "Shifting regions..."
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()
def fix_regions(self):
"""
Overwrites the region polygons with fixed
versions if found to be invalid (according to Shapely).
"""
for region in self.regions:
if not region['polygon'].is_valid:
region['polygon'] = region['polygon'].buffer(0)
def buffer_paths(self):
"""
This is part of the parsing process. "Thickens" the paths
by their appertures. This will only work for circular appertures.
:return: None
"""
self.buffered_paths = []
for path in self.paths:
width = self.apertures[path["aperture"]]["size"]
@@ -613,6 +675,7 @@ class Gerber (Geometry):
"""
Creates geometry for Gerber flashes (aperture on a single point).
"""
self.flash_geometry = []
for flash in self.flashes:
aperture = self.apertures[flash['aperture']]
@@ -660,6 +723,7 @@ class Gerber (Geometry):
:rtype : None
:return: None
"""
# if len(self.buffered_paths) == 0:
# self.buffer_paths()
print "... buffer_paths()"
@@ -669,10 +733,9 @@ class Gerber (Geometry):
print "... do_flashes()"
self.do_flashes()
print "... cascaded_union()"
self.solid_geometry = cascaded_union(
self.buffered_paths +
[poly['polygon'] for poly in self.regions] +
self.flash_geometry)
self.solid_geometry = cascaded_union(self.buffered_paths +
[poly['polygon'] for poly in self.regions] +
self.flash_geometry)
def get_bounding_box(self, margin=0.0, rounded=False):
"""
@@ -688,6 +751,7 @@ class Gerber (Geometry):
:return: The bounding box.
:rtype: Shapely.Polygon
"""
bbox = self.solid_geometry.envelope.buffer(margin)
if not rounded:
bbox = bbox.envelope
@@ -710,12 +774,14 @@ class Excellon(Geometry):
tool (str) A key in ``tools``
================ ====================================
"""
def __init__(self):
"""
The constructor takes no parameters.
:return: Excellon object.
:rtype: Excellon
"""
Geometry.__init__(self)
self.tools = {}
@@ -820,6 +886,25 @@ class Excellon(Geometry):
for drill in self.drills:
drill['point'] = affinity.scale(drill['point'], factor, factor, origin=(0, 0))
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()
def convert_units(self, units):
factor = Geometry.convert_units(self, units)
@@ -1095,6 +1180,8 @@ class CNCjob(Geometry):
fast or feedrate speed.
"""
kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
# Results go here
geometry = []
@@ -1103,22 +1190,31 @@ class CNCjob(Geometry):
# Last known instruction
current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
# Current path: temporary storage until tool is
# lifted or lowered.
path = []
# Process every instruction
for gobj in gobjs:
# Changing height:
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']
# 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.
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']
@@ -1129,7 +1225,9 @@ class CNCjob(Geometry):
y = gobj['Y']
else:
y = current['Y']
kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
if current['Z'] > 0:
kind[0] = 'T'
if current['G'] > 0:
@@ -1137,23 +1235,26 @@ class CNCjob(Geometry):
arcdir = [None, None, "cw", "ccw"]
if current['G'] in [0, 1]: # line
geometry.append({'geom': LineString([(current['X'], current['Y']),
(x, y)]), 'kind': kind})
path.append((x, y))
# geometry.append({'geom': LineString([(current['X'], current['Y']),
# (x, y)]), 'kind': kind})
if current['G'] in [2, 3]: # arc
center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
radius = sqrt(gobj['I']**2 + gobj['J']**2)
start = arctan2(-gobj['J'], -gobj['I'])
stop = arctan2(-center[1]+y, -center[0]+x)
geometry.append({'geom': arc(center, radius, start, stop,
arcdir[current['G']],
self.steps_per_circ),
'kind': kind})
path += arc(center, radius, start, stop,
arcdir[current['G']],
self.steps_per_circ)
# geometry.append({'geom': arc(center, radius, start, stop,
# arcdir[current['G']],
# self.steps_per_circ),
# 'kind': kind})
# Update current instruction
for code in gobj:
current[code] = gobj[code]
#self.G_geometry = geometry
self.gcode_parsed = geometry
return geometry
@@ -1194,7 +1295,7 @@ class CNCjob(Geometry):
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}, tool_tolerance=0.001):
alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005):
"""
Plots the G-code job onto the given axes.
@@ -1320,6 +1421,21 @@ class CNCjob(Geometry):
self.create_geometry()
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()
def get_bounds(geometry_set):
xmin = Inf
@@ -1343,7 +1459,7 @@ def get_bounds(geometry_set):
def arc(center, radius, start, stop, direction, steps_per_circ):
"""
Creates a Shapely.LineString for the specified arc.
Creates a list of point along the specified arc.
:param center: Coordinates of the center [x, y]
:type center: list
@@ -1358,9 +1474,11 @@ def arc(center, radius, start, stop, direction, steps_per_circ):
:param steps_per_circ: Number of straight line segments to
represent a circle.
:type steps_per_circ: int
:return: The desired arc.
:rtype: Shapely.LineString
:return: The desired arc, as list of tuples
:rtype: list
"""
# TODO: Resolution should be established by fraction of total length, not angle.
da_sign = {"cw": -1.0, "ccw": 1.0}
points = []
if direction == "ccw" and stop <= start:
@@ -1375,8 +1493,8 @@ def arc(center, radius, start, stop, direction, steps_per_circ):
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)
points.append((center[0]+radius*cos(theta), center[1]+radius*sin(theta)))
return points
def clear_poly(poly, tooldia, overlap=0.1):