diff --git a/FlatCAM.py b/FlatCAM.py
index 2f82859e..711fb9e7 100644
--- a/FlatCAM.py
+++ b/FlatCAM.py
@@ -61,6 +61,7 @@ class FlatCAMObj:
:return: None
:rtype: None
"""
+
if self.axes is None:
print "New axes"
self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
@@ -177,15 +178,17 @@ class FlatCAMObj:
return
print "Unknown kind of form item:", fkind
- # def plot(self, figure):
- # """
- # Extend this method! Sets up axes if needed and
- # clears them. Descendants must do the actual plotting.
- # """
- # # Creates the axes if necessary and sets them up.
- # self.setup_axes(figure)
-
def plot(self):
+ """
+ Plot this object (Extend this method to implement the actual plotting).
+ Axes get created, appended to canvas and cleared before plotting.
+ Call this in descendants before doing the plotting.
+
+ :return: Whether to continue plotting or not depending on the "plot" option.
+ :rtype: bool
+ """
+
+ # Axes must exist and be attached to canvas.
if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes:
self.axes = self.app.plotcanvas.new_axes(self.options['name'])
@@ -194,6 +197,9 @@ class FlatCAMObj:
self.app.plotcanvas.auto_adjust_axes()
return False
+ # Clear axes or we will plot on top of them.
+ self.axes.cla()
+ # GLib.idle_add(self.axes.cla)
return True
def serialize(self):
@@ -237,10 +243,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
"isotooldia": 0.016,
"isopasses": 1,
"isooverlap": 0.15,
+ "cutouttooldia": 0.07,
"cutoutmargin": 0.2,
"cutoutgapsize": 0.15,
"gaps": "tb",
"noncoppermargin": 0.0,
+ "noncopperrounded": False,
"bboxmargin": 0.0,
"bboxrounded": False
})
@@ -254,10 +262,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
"isotooldia": "entry_eval",
"isopasses": "entry_eval",
"isooverlap": "entry_eval",
+ "cutouttooldia": "entry_eval",
"cutoutmargin": "entry_eval",
"cutoutgapsize": "entry_eval",
"gaps": "radio",
"noncoppermargin": "entry_eval",
+ "noncopperrounded": "cb",
"bboxmargin": "entry_eval",
"bboxrounded": "cb"
})
@@ -325,7 +335,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
x, y = ints.coords.xy
self.axes.plot(x, y, linespec)
- self.app.plotcanvas.auto_adjust_axes()
+ # self.app.plotcanvas.auto_adjust_axes()
+ GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
def serialize(self):
return {
@@ -348,7 +359,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
self.options.update({
"plot": True,
"solid": False,
- "multicolored": False,
"drillz": -0.1,
"travelz": 0.1,
"feedrate": 5.0,
@@ -358,7 +368,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
self.form_kinds.update({
"plot": "cb",
"solid": "cb",
- "multicolored": "cb",
"drillz": "entry_eval",
"travelz": "entry_eval",
"feedrate": "entry_eval",
@@ -393,12 +402,21 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
self.solid_geometry = [self.solid_geometry]
# Plot excellon (All polygons?)
- for geo in self.solid_geometry:
- x, y = geo.exterior.coords.xy
- self.axes.plot(x, y, 'r-')
- for ints in geo.interiors:
- x, y = ints.coords.xy
- self.axes.plot(x, y, 'g-')
+ if self.options["solid"]:
+ for geo in self.solid_geometry:
+ patch = PolygonPatch(geo,
+ facecolor="#C40000",
+ edgecolor="#750000",
+ alpha=0.75,
+ zorder=3)
+ self.axes.add_patch(patch)
+ else:
+ for geo in self.solid_geometry:
+ x, y = geo.exterior.coords.xy
+ self.axes.plot(x, y, 'r-')
+ for ints in geo.interiors:
+ x, y = ints.coords.xy
+ self.axes.plot(x, y, 'g-')
self.app.plotcanvas.auto_adjust_axes()
@@ -914,8 +932,10 @@ class App:
percentage += delta
GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
- app_obj.plotcanvas.auto_adjust_axes()
- self.on_zoom_fit(None)
+ #app_obj.plotcanvas.auto_adjust_axes()
+ #self.on_zoom_fit(None)
+ GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
+ GLib.idle_add(lambda: self.on_zoom_fit(None))
GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
t = threading.Thread(target=thread_func, args=(self,))
@@ -1340,7 +1360,7 @@ class App:
"cb_gerber_plot": "Plot this object on the main window.",
"cb_gerber_mergepolys": "Show overlapping polygons as single.",
"cb_gerber_solid": "Paint inside polygons.",
- "cb_gerber_multicolored": "Draw polygons with different polygons."
+ "cb_gerber_multicolored": "Draw polygons with different colors."
}
for widget in tooltips:
@@ -1961,7 +1981,7 @@ class App:
#GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Plotting..."))
#GLib.idle_add(lambda: app_obj.get_current().plot(app_obj.figure))
obj.plot()
- GLib.idle_add(lambda: app_obj.on_zoom_fit(None))
+ #GLib.idle_add(lambda: app_obj.on_zoom_fit(None))
GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
t = threading.Thread(target=thread_func, args=(self,))
@@ -2058,6 +2078,8 @@ class App:
def geo_init(geo_obj, app_obj):
assert isinstance(geo_obj, FlatCAMGeometry)
bounding_box = gerb.solid_geometry.envelope.buffer(gerb.options["noncoppermargin"])
+ if not gerb.options["noncopperrounded"]:
+ bounding_box = bounding_box.envelope
non_copper = bounding_box.difference(gerb.solid_geometry)
geo_obj.solid_geometry = non_copper
@@ -2078,8 +2100,8 @@ class App:
name = gerb.options["name"] + "_cutout"
def geo_init(geo_obj, app_obj):
- margin = gerb.options["cutoutmargin"]
- gap_size = gerb.options["cutoutgapsize"]
+ margin = gerb.options["cutoutmargin"] + gerb.options["cutouttooldia"]/2
+ gap_size = gerb.options["cutoutgapsize"] + gerb.options["cutouttooldia"]
minx, miny, maxx, maxy = gerb.bounds()
minx -= margin
maxx += margin
@@ -2990,7 +3012,7 @@ class PlotCanvas:
width = xmax - xmin
height = ymax - ymin
- if center is None:
+ if center is None or center == [None, None]:
center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
# For keeping the point at the pointer location
diff --git a/FlatCAM.ui b/FlatCAM.ui
index 9d2f8647..1feaeb7f 100644
--- a/FlatCAM.ui
+++ b/FlatCAM.ui
@@ -112,366 +112,6 @@ THE SOFTWARE.
False
gtk-open
-
False
@@ -540,6 +180,8 @@ THE SOFTWARE.
True
False
+ The Object's name. Only gets changed
+after hitting Enter in the text box.
3
Name:
@@ -593,6 +235,7 @@ THE SOFTWARE.
True
True
False
+ Plot this object in the main window.
0
True
True
@@ -621,6 +264,7 @@ THE SOFTWARE.
True
False
+ Tool diameter used for plotting.
1
Tool diam:
@@ -670,6 +314,8 @@ THE SOFTWARE.
True
False
+ <b>Export G-Code:</b> Save a
+G-Code file for this CNC job.
3
0
3
@@ -690,6 +336,7 @@ THE SOFTWARE.
True
True
True
+ Save a G-Code file for this CNC job.
@@ -703,6 +350,9 @@ THE SOFTWARE.
True
False
+ <b>Scale:</b> Resizes the geometry
+of the object. All sizes and coordinates
+are multiplied by the given factor.
3
0
3
@@ -728,6 +378,8 @@ THE SOFTWARE.
True
False
+ Factor by which to multiply all
+geometrical dimensions.
1
Factor:
@@ -764,6 +416,8 @@ THE SOFTWARE.
True
True
True
+ Scales the geometry
+of this object.
@@ -777,6 +431,8 @@ THE SOFTWARE.
True
False
+ <b>Offset:</b> Shift the geometry of
+this object by the specified (x, y) vector.
3
0
3
@@ -802,6 +458,10 @@ THE SOFTWARE.
True
False
+ Vector by which to offset the geometry.
+Format is <b>(x, y)</b>, where <b>x</b> and <b>y</b> are
+decimal numbers representing the
+distance in the corresponding axis.
1
Offset Vector:
@@ -838,6 +498,8 @@ THE SOFTWARE.
True
True
True
+ Offset the geometry of
+this object.
@@ -1010,19 +672,7 @@ THE SOFTWARE.
-
- Multi-colored
- True
- True
- False
- 0
- True
-
-
- False
- True
- 5
-
+
@@ -1458,6 +1108,8 @@ THE SOFTWARE.
True
False
+ The Object's name. Only gets changed
+after hitting Enter in the text box.
3
Name:
@@ -1511,6 +1163,7 @@ THE SOFTWARE.
True
True
False
+ Plot this object on the main window.
0
True
True
@@ -1528,6 +1181,7 @@ THE SOFTWARE.
True
True
False
+ Show overlapping polygons as single.
0
True
@@ -1543,6 +1197,7 @@ THE SOFTWARE.
True
True
False
+ Draw polygons with different colors.
0
True
@@ -1571,6 +1226,8 @@ THE SOFTWARE.
True
False
+ <b>Create CNC Job:</b> CNC Jobs from Geometry
+Objects are cutting toolpaths along object contours.
5
0
3
@@ -1640,6 +1297,10 @@ THE SOFTWARE.
True
False
+ The contours are cut at the given Z-axis
+position. Typically, the surface of the
+workpiece is at z=0.0, and <b>Cut Z</b>
+is specified as a negative number.
1
Cut Z:
@@ -1654,6 +1315,9 @@ THE SOFTWARE.
True
False
+ This specifies the Z-axis position of the
+tool for XY-plane motions when not
+cutting.
1
Travel Z:
@@ -1668,6 +1332,9 @@ THE SOFTWARE.
True
False
+ The feed rate is the speed in <b>units/minute</b>
+when cutting. When not cutting, movement is
+done at maximum speed.
1
Feed rate:
@@ -1682,6 +1349,9 @@ THE SOFTWARE.
True
False
+ The tool diameter specified here is for
+visual purposes only and does not affect
+the resulting job.
1
Tool diam:
@@ -1720,6 +1390,8 @@ THE SOFTWARE.
True
True
True
+ Creates the CNC Job oobject from this
+Geometry object with the specified options.
@@ -1733,6 +1405,10 @@ THE SOFTWARE.
True
False
+ <b>Paint Area:</b> Creates contour
+inside a polygon for a tool to cover
+its entire surface. Use for clearing
+large copper areas.
5
0
3
@@ -1757,6 +1433,8 @@ THE SOFTWARE.
True
False
+ Tool diameter for painting
+inside a polygon.
1
Tool diam:
@@ -1786,6 +1464,8 @@ THE SOFTWARE.
True
False
+ How much (fraction of tool diameter)
+to overlap each toolpath.
1
Overlap:
@@ -1815,6 +1495,10 @@ THE SOFTWARE.
True
False
+ Margin from the edge of the polygon.
+Use this to stay away from edges
+cut or to be cut with a finer, more
+accurate tool.
1
Margin:
@@ -1853,6 +1537,12 @@ THE SOFTWARE.
True
True
True
+ Creates a Geometry Object with toolpaths
+to cover the inside of a polygon.
+
+<b>Note:</b> After clicking generate
+you have to click inside the desired
+polygon.
@@ -1866,6 +1556,9 @@ THE SOFTWARE.
True
False
+ <b>Scale:</b> Resizes the geometry
+of the object. All sizes and coordinates
+are multiplied by the given factor.
3
0
3
@@ -1891,6 +1584,8 @@ THE SOFTWARE.
True
False
+ Factor by which to multiply all
+geometrical dimensions.
1
Factor:
@@ -1927,6 +1622,8 @@ THE SOFTWARE.
True
True
True
+ Scales the geometry
+of this object.
@@ -1940,6 +1637,8 @@ THE SOFTWARE.
True
False
+ <b>Offset:</b> Shift the geometry of
+this object by the specified (x, y) vector.
3
0
3
@@ -1965,6 +1664,10 @@ THE SOFTWARE.
True
False
+ Vector by which to offset the geometry.
+Format is <b>(x, y)</b>, where <b>x</b> and <b>y</b> are
+decimal numbers representing the
+distance in the corresponding axis.
1
Offset Vector:
@@ -2001,6 +1704,8 @@ THE SOFTWARE.
True
True
True
+ Offset the geometry of
+this object.
@@ -2091,6 +1796,8 @@ THE SOFTWARE.
True
False
+ The Object's name. Only gets changed
+after hitting Enter in the text box.
3
Name:
@@ -2412,7 +2119,7 @@ cutting tool path.
0
- 0
+ 1
1
1
@@ -2428,7 +2135,7 @@ cutting tool path.
1
- 0
+ 1
1
1
@@ -2444,7 +2151,7 @@ cutting tool path.
1
- 1
+ 2
1
1
@@ -2461,7 +2168,7 @@ board in place until the job is complete.
0
- 1
+ 2
1
1
@@ -2475,7 +2182,7 @@ board in place until the job is complete.
0
- 2
+ 3
1
1
@@ -2543,7 +2250,39 @@ of the board cutout.
1
- 2
+ 3
+ 1
+ 1
+
+
+
+
+ True
+ False
+ True
+ Tool diameter to be used to cut out
+the baord.
+ 1
+ Tool diam.:
+
+
+ 0
+ 0
+ 1
+ 1
+
+
+
+
+ True
+ True
+ ●
+ 12
+ True
+
+
+ 1
+ 0
1
1
@@ -2644,6 +2383,28 @@ this boundary.
15
+
+
+ Rounded corners
+ True
+ True
+ False
+ True
+ Whether to draw rounded corners on the bounding
+box. The radius is the Boundary margin.
+
+ Whether to draw rounded corners on the bounding
+box. The radius is the Boundary margin.
+
+ 0
+ True
+
+
+ False
+ True
+ 16
+
+
Generate Geometry
@@ -2658,7 +2419,7 @@ areas without copper.
False
True
- 16
+ 17
@@ -2678,7 +2439,7 @@ boundary around the PCB contents.
False
True
- 17
+ 18
@@ -2723,7 +2484,7 @@ bounding box.
False
True
- 18
+ 19
@@ -2741,7 +2502,7 @@ box. The radius is the Boundary margin.
False
True
- 19
+ 20
@@ -2758,7 +2519,7 @@ with the bounding box.
False
True
- 20
+ 21
@@ -2779,7 +2540,7 @@ are multiplied by the given factor.
False
True
- 21
+ 22
@@ -2822,7 +2583,7 @@ geometrical dimensions.
False
True
- 22
+ 23
@@ -2839,7 +2600,7 @@ of this object.
False
True
- 23
+ 24
@@ -2859,7 +2620,7 @@ this object by the specified (x, y) vector.
False
True
- 24
+ 25
@@ -2904,7 +2665,7 @@ distance in the corresponding axis.
False
True
- 25
+ 26
@@ -2921,7 +2682,364 @@ this object.
False
True
- 26
+ 27
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ False
+
+
+ True
+ True
+ never
+ in
+
+
+ True
+ False
+
+
+ True
+ False
+ 5
+ 5
+ 5
+ 5
+ vertical
+
+
+ True
+ False
+ 6
+ 3
+ Double-Sided PCB Tool
+
+
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ True
+ False
+ 3
+ 3
+
+
+ True
+ False
+ 1
+ 3
+ Bottom Layer:
+
+
+ 0
+ 0
+ 1
+ 1
+
+
+
+
+ 200
+ True
+ False
+ start
+ 0
+ 1
+
+
+ 1
+ 0
+ 1
+ 1
+
+
+
+
+ True
+ False
+ 1
+ 3
+ Mirror Axis:
+
+
+ 0
+ 1
+ 1
+ 1
+
+
+
+
+ True
+ False
+ 10
+
+
+ X
+ True
+ True
+ False
+ 0
+ True
+ True
+
+
+ False
+ True
+ 0
+
+
+
+
+ Y
+ True
+ True
+ False
+ 0
+ True
+ rb_mirror_x
+
+
+ False
+ True
+ 1
+
+
+
+
+ 1
+ 1
+ 1
+ 1
+
+
+
+
+ True
+ False
+ 1
+ 3
+ Axis location:
+
+
+ 0
+ 2
+ 1
+ 1
+
+
+
+
+ True
+ False
+ 10
+
+
+ Point
+ True
+ True
+ False
+ 0
+ True
+ True
+
+
+
+ False
+ True
+ 0
+
+
+
+
+ Box
+ True
+ True
+ False
+ 0
+ True
+ rb_mirror_point
+
+
+ False
+ True
+ 1
+
+
+
+
+ 1
+ 2
+ 1
+ 1
+
+
+
+
+ True
+ False
+ 1
+ 3
+ Point/Box:
+
+
+ 0
+ 3
+ 1
+ 1
+
+
+
+
+ True
+ False
+ vertical
+
+
+
+
+
+ 1
+ 3
+ 1
+ 1
+
+
+
+
+ True
+ False
+ 1
+ 3
+ Algnmt holes:
+
+
+ 0
+ 4
+ 1
+ 1
+
+
+
+
+ True
+ True
+ ●
+
+
+ 1
+ 4
+ 1
+ 1
+
+
+
+
+ True
+ False
+ 1
+ 3
+ Drill diam.:
+
+
+ 0
+ 5
+ 1
+ 1
+
+
+
+
+ True
+ True
+ ●
+ True
+
+
+ 1
+ 5
+ 1
+ 1
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+ True
+ False
+ end
+ 6
+ 3
+
+
+ Create Alignment Drill
+ 120
+ True
+ True
+ True
+ end
+
+
+
+
+ False
+ False
+ 4
+ 0
+
+
+
+
+ Create Mirror
+ 120
+ True
+ True
+ True
+ end
+
+
+
+
+ False
+ False
+ 4
+ 1
+
+
+
+
+ False
+ True
+ 2
@@ -3807,7 +3925,7 @@ to overlap each pass.
0
- 0
+ 1
1
1
@@ -3823,7 +3941,7 @@ to overlap each pass.
1
- 0
+ 1
1
1
@@ -3839,7 +3957,7 @@ to overlap each pass.
1
- 1
+ 2
1
1
@@ -3853,7 +3971,7 @@ to overlap each pass.
0
- 1
+ 2
1
1
@@ -3867,7 +3985,7 @@ to overlap each pass.
0
- 2
+ 3
1
1
@@ -3931,7 +4049,36 @@ to overlap each pass.
1
- 2
+ 3
+ 1
+ 1
+
+
+
+
+ True
+ False
+ 1
+ Tool diam.:
+
+
+ 0
+ 0
+ 1
+ 1
+
+
+
+
+ True
+ True
+ ●
+ 12
+ True
+
+
+ 1
+ 0
1
1
@@ -4003,6 +4150,21 @@ to overlap each pass.
10
+
+
+ Rounded corners
+ True
+ True
+ False
+ 0
+ True
+
+
+ False
+ True
+ 11
+
+
True
@@ -4018,7 +4180,7 @@ to overlap each pass.
False
True
- 11
+ 12
@@ -4060,7 +4222,7 @@ to overlap each pass.
False
True
- 12
+ 13
@@ -4076,7 +4238,7 @@ to overlap each pass.
False
True
- 13
+ 14
@@ -4085,9 +4247,6 @@ to overlap each pass.
-
-
-
@@ -4184,20 +4343,7 @@ to overlap each pass.
-
- Multi-colored
- True
- True
- False
- 0
- True
-
-
-
- False
- True
- 3
-
+
diff --git a/camlib.py b/camlib.py
index 76c5e403..16ccb453 100644
--- a/camlib.py
+++ b/camlib.py
@@ -180,6 +180,336 @@ class Geometry:
setattr(self, attr, d[attr])
+class ApertureMacro:
+
+ ## Regular expressions
+ am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$')
+ am2_re = re.compile(r'(.*)%$')
+ amcomm_re = re.compile(r'^0(.*)')
+ amprim_re = re.compile(r'^[1-9].*')
+ amvar_re = re.compile(r'^\$([0-9a-zA-z]+)=(.*)')
+
+ def __init__(self, name=None):
+ self.name = name
+ self.raw = ""
+ self.primitives = []
+ self.locvars = {}
+ self.geometry = None
+
+ def parse_content(self):
+ """
+ Creates numerical lists for all primitives in the aperture
+ macro (in ``self.raw``) by replacing all variables by their
+ values iteratively and evaluating expressions. Results
+ are stored in ``self.primitives``.
+
+ :return: None
+ """
+ # Cleanup
+ self.raw = self.raw.replace('\n', '').replace('\r', '').strip(" *")
+ self.primitives = []
+
+ # Separate parts
+ parts = self.raw.split('*')
+
+ #### Every part in the macro ####
+ for part in parts:
+ ### Comments. Ignored.
+ match = ApertureMacro.amcomm_re.search(part)
+ if match:
+ continue
+
+ ### Variables
+ # These are variables defined locally inside the macro. They can be
+ # numerical constant or defind in terms of previously define
+ # variables, which can be defined locally or in an aperture
+ # definition. All replacements ocurr here.
+ match = ApertureMacro.amvar_re.search(part)
+ if match:
+ var = match.group(1)
+ val = match.group(2)
+
+ # Replace variables in value
+ for v in self.locvars:
+ val = re.sub(r'\$'+str(v)+r'(?![0-9a-zA-Z])', str(self.locvars[v]), val)
+
+ # Make all others 0
+ val = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", val)
+
+ # Change x with *
+ val = re.sub(r'x', "\*", val)
+
+ # Eval() and store.
+ self.locvars[var] = eval(val)
+ continue
+
+ ### Primitives
+ # Each is an array. The first identifies the primitive, while the
+ # rest depend on the primitive. All are strings representing a
+ # number and may contain variable definition. The values of these
+ # variables are defined in an aperture definition.
+ match = ApertureMacro.amprim_re.search(part)
+ if match:
+ ## Replace all variables
+ for v in self.locvars:
+ part = re.sub(r'\$'+str(v)+r'(?![0-9a-zA-Z])', str(self.locvars[v]), part)
+
+ # Make all others 0
+ part = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", part)
+
+ # Change x with *
+ part = re.sub(r'x', "\*", part)
+
+ ## Store
+ elements = part.split(",")
+ self.primitives.append([eval(x) for x in elements])
+ continue
+
+ print "WARNING: Unknown syntax of aperture macro part:", part
+
+ def append(self, data):
+ """
+ Appends a string to the raw macro.
+
+ :param data: Part of the macro.
+ :type data: str
+ :return: None
+ """
+ self.raw += data
+
+ @staticmethod
+ def default2zero(n, mods):
+ """
+ Pads the ``mods`` list with zeros resulting in an
+ list of length n.
+
+ :param n: Length of the resulting list.
+ :type n: int
+ :param mods: List to be padded.
+ :type mods: list
+ :return: Zero-padded list.
+ :rtype: list
+ """
+ x = [0.0]*n
+ na = len(mods)
+ x[0:na] = mods
+ return x
+
+ @staticmethod
+ def make_circle(mods):
+ """
+
+ :param mods: (Exposure 0/1, Diameter >=0, X-coord, Y-coord)
+ :return:
+ """
+
+ pol, dia, x, y = ApertureMacro.default2zero(4, mods)
+
+ return {"pol": int(pol), "geometry": Point(x, y).buffer(dia/2)}
+
+ @staticmethod
+ def make_vectorline(mods):
+ """
+
+ :param mods: (Exposure 0/1, Line width >= 0, X-start, Y-start, X-end, Y-end,
+ rotation angle around origin in degrees)
+ :return:
+ """
+ pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods)
+
+ line = LineString([(xs, ys), (xe, ye)])
+ box = line.buffer(width/2, cap_style=2)
+ box_rotated = affinity.rotate(box, angle, origin=(0, 0))
+
+ return {"pol": int(pol), "geometry": box_rotated}
+
+ @staticmethod
+ def make_centerline(mods):
+ """
+
+ :param mods: (Exposure 0/1, width >=0, height >=0, x-center, y-center,
+ rotation angle around origin in degrees)
+ :return:
+ """
+
+ pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
+
+ box = shply_box(x-width/2, y-height/2, x+width/2, y+height/2)
+ box_rotated = affinity.rotate(box, angle, origin=(0, 0))
+
+ return {"pol": int(pol), "geometry": box_rotated}
+
+ @staticmethod
+ def make_lowerleftline(mods):
+ """
+
+ :param mods: (exposure 0/1, width >=0, height >=0, x-lowerleft, y-lowerleft,
+ rotation angle around origin in degrees)
+ :return:
+ """
+
+ pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
+
+ box = shply_box(x, y, x+width, y+height)
+ box_rotated = affinity.rotate(box, angle, origin=(0, 0))
+
+ return {"pol": int(pol), "geometry": box_rotated}
+
+ @staticmethod
+ def make_outline(mods):
+ """
+
+ :param mods:
+ :return:
+ """
+
+ pol = mods[0]
+ n = mods[1]
+ points = [(0, 0)]*(n+1)
+
+ for i in range(n+1):
+ points[i] = mods[2*i + 2:2*i + 4]
+
+ angle = mods[2*n + 4]
+
+ poly = Polygon(points)
+ poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
+
+ return {"pol": int(pol), "geometry": poly_rotated}
+
+ @staticmethod
+ def make_polygon(mods):
+ """
+ Note: Specs indicate that rotation is only allowed if the center
+ (x, y) == (0, 0). I will tolerate breaking this rule.
+
+ :param mods: (exposure 0/1, n_verts 3<=n<=12, x-center, y-center,
+ diameter of circumscribed circle >=0, rotation angle around origin)
+ :return:
+ """
+
+ pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods)
+ points = [(0, 0)]*nverts
+
+ for i in nverts:
+ points[i] = (x + 0.5 * dia * cos(2*pi * i/nverts),
+ y + 0.5 * dia * sin(2*pi * i/nverts))
+
+ poly = Polygon(points)
+ poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
+
+ return {"pol": int(pol), "geometry": poly_rotated}
+
+ @staticmethod
+ def make_moire(mods):
+ """
+ Note: Specs indicate that rotation is only allowed if the center
+ (x, y) == (0, 0). I will tolerate breaking this rule.
+
+ :param mods: (x-center, y-center, outer_dia_outer_ring, ring thickness,
+ gap, max_rings, crosshair_thickness, crosshair_len, rotation
+ angle around origin in degrees)
+ :return:
+ """
+
+ x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods)
+
+ r = dia/2 - thickness/2
+ result = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
+ ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0) # Need a copy!
+
+ i = 1 # Number of rings created so far
+
+ ## If the ring does not have an interior it means that it is
+ ## a disk. Then stop.
+ while len(ring.interiors) > 0 and i < nrings:
+ r -= thickness + gap
+ if r <= 0:
+ break
+ ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
+ result = cascaded_union([result, ring])
+ i += 1
+
+ ## Crosshair
+ hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th/2.0, cap_style=2)
+ ver = LineString([(x, y-cross_len), (x, y + cross_len)]).buffer(cross_th/2.0, cap_style=2)
+ result = cascaded_union([result, hor, ver])
+
+ return {"pol": 1, "geometry": result}
+
+ @staticmethod
+ def make_thermal(mods):
+ """
+ Note: Specs indicate that rotation is only allowed if the center
+ (x, y) == (0, 0). I will tolerate breaking this rule.
+
+ :param mods: [x-center, y-center, diameter-outside, diameter-inside,
+ gap-thickness, rotation angle around origin]
+ :return:
+ """
+
+ x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods)
+
+ ring = Point((x, y)).buffer(dout/2.0).difference(Point((x, y)).buffer(din/2.0))
+ hline = LineString([(x - dout/2.0, y), (x + dout/2.0, y)]).buffer(t/2.0, cap_style=3)
+ vline = LineString([(x, y - dout/2.0), (x, y + dout/2.0)]).buffer(t/2.0, cap_style=3)
+ thermal = ring.difference(hline.union(vline))
+
+ return {"pol": 1, "geometry": thermal}
+
+ def make_geometry(self, modifiers):
+ """
+ Runs the macro for the given modifiers and generates
+ the corresponding geometry.
+
+ :param modifiers: Modifiers (parameters) for this macro
+ :type modifiers: list
+ """
+
+ ## Primitive makers
+ makers = {
+ "1": ApertureMacro.make_circle,
+ "2": ApertureMacro.make_vectorline,
+ "20": ApertureMacro.make_vectorline,
+ "21": ApertureMacro.make_centerline,
+ "22": ApertureMacro.make_lowerleftline,
+ "4": ApertureMacro.make_outline,
+ "5": ApertureMacro.make_polygon,
+ "6": ApertureMacro.make_moire,
+ "7": ApertureMacro.make_thermal
+ }
+
+ ## Store modifiers as local variables
+ modifiers = modifiers or []
+ modifiers = [float(m) for m in modifiers]
+ self.locvars = {}
+ for i in range(1, len(modifiers)+1):
+ self.locvars[str(i)] = modifiers[i]
+
+ ## Parse
+ self.primitives = [] # Cleanup
+ self.geometry = None
+ self.parse_content()
+
+ ## Make the geometry
+ for primitive in self.primitives:
+ # Make the primitive
+ prim_geo = makers[str(int(primitive[0]))](primitive[1:])
+
+ # Add it (according to polarity)
+ if self.geometry is None and prim_geo['pol'] == 1:
+ self.geometry = prim_geo['geometry']
+ continue
+ if prim_geo['pol'] == 1:
+ self.geometry = self.geometry.union(prim_geo['geometry'])
+ continue
+ if prim_geo['pol'] == 0:
+ self.geometry = self.geometry.difference(prim_geo['geometry'])
+ continue
+
+ return self.geometry
+
+
class Gerber (Geometry):
"""
**ATTRIBUTES**
@@ -191,7 +521,7 @@ class Gerber (Geometry):
+-----------+-----------------------------------+
| Key | Value |
+===========+===================================+
- | type | (str) "C", "R", or "O" |
+ | type | (str) "C", "R", "O", "P", or "AP" |
+-----------+-----------------------------------+
| others | Depend on ``type`` |
+-----------+-----------------------------------+
@@ -251,9 +581,11 @@ class Gerber (Geometry):
"""
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
"""
+
# Initialize parent
Geometry.__init__(self)
@@ -287,6 +619,10 @@ class Gerber (Geometry):
# Geometry from flashes
self.flash_geometry = []
+ # Aperture Macros
+ # TODO: Make sure these can be serialized
+ self.aperture_macros = {}
+
# Attributes to be included in serialization
# Always append to it because it carries contents
# from Geometry.
@@ -308,7 +644,7 @@ class Gerber (Geometry):
self.comm_re = re.compile(r'^G0?4(.*)$')
# AD - Aperture definition
- self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z0-9]*),(.*)\*%$')
+ self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z0-9]*)(?:,(.*))?\*%$')
# AM - Aperture Macro
# Beginning of macro (Ends with *%):
@@ -356,6 +692,16 @@ class Gerber (Geometry):
# LP - Level polarity
self.lpol_re = re.compile(r'^%LP([DC])\*%$')
+ # Units (OBSOLETE)
+ self.units_re = re.compile(r'^G7([01])\*$')
+
+ # Absolute/Relative G90/1 (OBSOLETE)
+ self.absrel_re = re.compile(r'^G9([01])\*$')
+
+ # Aperture macros
+ self.am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$')
+ self.am2_re = re.compile(r'(.*)%$')
+
# TODO: This is bad.
self.steps_per_circ = 40
@@ -376,9 +722,8 @@ class Gerber (Geometry):
:rtype : None
"""
- # Apertures
- #print "Scaling apertures..."
- #List of the non-dimension aperture parameters
+ ## Apertures
+ # List of the non-dimension aperture parameters
nonDimensions = ["type", "nVertices", "rotation"]
for apid in self.apertures:
for param in self.apertures[apid]:
@@ -386,21 +731,18 @@ class Gerber (Geometry):
print "Tool:", apid, "Parameter:", param
self.apertures[apid][param] *= factor
- # Paths
- #print "Scaling paths..."
+ ## Paths
for path in self.paths:
path['linestring'] = affinity.scale(path['linestring'],
factor, factor, origin=(0, 0))
- # Flashes
- #print "Scaling flashes..."
+ ## Flashes
for fl in self.flashes:
# TODO: Shouldn't 'loc' be a numpy.array()?
fl['loc'][0] *= factor
fl['loc'][1] *= factor
- # Regions
- #print "Scaling regions..."
+ ## Regions
for reg in self.regions:
reg['polygon'] = affinity.scale(reg['polygon'], factor, factor,
origin=(0, 0))
@@ -419,6 +761,7 @@ class Gerber (Geometry):
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
@@ -426,21 +769,18 @@ class Gerber (Geometry):
dx, dy = vect
- # Paths
- #print "Shifting paths..."
+ ## Paths
for path in self.paths:
path['linestring'] = affinity.translate(path['linestring'],
xoff=dx, yoff=dy)
- # Flashes
- #print "Shifting flashes..."
+ ## 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..."
+ ## Regions
for reg in self.regions:
reg['polygon'] = affinity.translate(reg['polygon'],
xoff=dx, yoff=dy)
@@ -452,6 +792,8 @@ class Gerber (Geometry):
"""
Overwrites the region polygons with fixed
versions if found to be invalid (according to Shapely).
+
+ :return: None
"""
for region in self.regions:
@@ -462,6 +804,7 @@ class Gerber (Geometry):
"""
This is part of the parsing process. "Thickens" the paths
by their appertures. This will only work for circular appertures.
+
:return: None
"""
@@ -483,6 +826,7 @@ class Gerber (Geometry):
* *Rectangle (R)*: width (float), height (float)
* *Obround (O)*: width (float), height (float).
* *Polygon (P)*: diameter(float), vertices(int), [rotation(float)]
+ * *Aperture Macro (AM)*: macro (ApertureMacro), modifiers (list)
:param apertureId: Id of the aperture being defined.
:param apertureType: Type of the aperture.
@@ -497,33 +841,43 @@ class Gerber (Geometry):
# 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(apertureId))
- paramList = apParameters.split('X')
- if apertureType == "C" : # Circle, example: %ADD11C,0.1*%
+ try: # Could be empty for aperture macros
+ paramList = apParameters.split('X')
+ except:
+ paramList = None
+
+ if apertureType == "C": # Circle, example: %ADD11C,0.1*%
self.apertures[apid] = {"type": "C",
"size": float(paramList[0])}
return apid
- if apertureType == "R" : # Rectangle, example: %ADD15R,0.05X0.12*%
+ if apertureType == "R": # Rectangle, example: %ADD15R,0.05X0.12*%
self.apertures[apid] = {"type": "R",
"width": float(paramList[0]),
"height": float(paramList[1])}
return apid
- if apertureType == "O" : # Obround
+ if apertureType == "O": # Obround
self.apertures[apid] = {"type": "O",
"width": float(paramList[0]),
"height": float(paramList[1])}
return apid
- if apertureType == "P" :
+ if apertureType == "P": # Polygon (regular)
self.apertures[apid] = {"type": "P",
"diam": float(paramList[0]),
"nVertices": int(paramList[1])}
- if len(paramList) >= 3 :
+ if len(paramList) >= 3:
self.apertures[apid]["rotation"] = float(paramList[2])
return apid
+ if apertureType in self.aperture_macros:
+ self.apertures[apid] = {"type": "AM",
+ "macro": self.aperture_macros[apertureType],
+ "modifiers": paramList}
+ return apid
+
print "WARNING: Aperture not implemented:", apertureType
return None
@@ -531,6 +885,10 @@ class Gerber (Geometry):
"""
Calls Gerber.parse_lines() with array of lines
read from the given file.
+
+ :param filename: Gerber file to parse.
+ :type filename: str
+ :return: None
"""
gfile = open(filename, 'r')
gstr = gfile.readlines()
@@ -566,24 +924,60 @@ class Gerber (Geometry):
current_x = None
current_y = None
- # How to interprest circular interpolation: SINGLE or MULTI
+ # Absolute or Relative/Incremental coordinates
+ absolute = True
+
+ # How to interpret circular interpolation: SINGLE or MULTI
quadrant_mode = None
+ # Indicates we are parsing an aperture macro
+ current_macro = None
+
+ #### Parsing starts here ####
line_num = 0
for gline in glines:
line_num += 1
- ## G01 - Linear interpolation plus flashes
+ ### Aperture Macros
+ # Having this at the beggining will slow things down
+ # but macros can have complicated statements than could
+ # be caught by other ptterns.
+ if current_macro is None: # No macro started yet
+ match = self.am1_re.search(gline)
+ # Start macro if match, else not an AM, carry on.
+ if match:
+ current_macro = match.group(1)
+ self.aperture_macros[current_macro] = ApertureMacro(name=current_macro)
+ if match.group(2): # Append
+ self.aperture_macros[current_macro].append(match.group(2))
+ if match.group(3): # Finish macro
+ #self.aperture_macros[current_macro].parse_content()
+ current_macro = None
+ continue
+ else: # Continue macro
+ match = self.am2_re.search(gline)
+ if match: # Finish macro
+ self.aperture_macros[current_macro].append(match.group(1))
+ #self.aperture_macros[current_macro].parse_content()
+ current_macro = None
+ else: # Append
+ self.aperture_macros[current_macro].append(gline)
+ continue
+
+ ### G01 - Linear interpolation plus flashes
# Operation code (D0x) missing is deprecated... oh well I will support it.
+ # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$'
match = self.lin_re.search(gline)
if match:
- # 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
+ # Dxx alone?
+ # 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
+ # NOTE: Letting it continue allows it to react to the
+ # operation code.
# Parse coordinates
if match.group(2) is not None:
@@ -616,7 +1010,7 @@ class Gerber (Geometry):
continue
- ## G02/3 - Circular interpolation
+ ### G02/3 - Circular interpolation
# 2-clockwise, 3-counterclockwise
match = self.circ_re.search(gline)
if match:
@@ -697,7 +1091,7 @@ class Gerber (Geometry):
if quadrant_mode == 'SINGLE':
print "Warning: Single quadrant arc are not implemented yet. (%d)" % line_num
- ## G74/75* - Single or multiple quadrant arcs
+ ### G74/75* - Single or multiple quadrant arcs
match = self.quad_re.search(gline)
if match:
if match.group(1) == '4':
@@ -706,7 +1100,7 @@ class Gerber (Geometry):
quadrant_mode = 'MULTI'
continue
- ## G37* - End region
+ ### G37* - End region
if self.regionoff_re.search(gline):
# Only one path defines region?
if len(path) < 3:
@@ -723,14 +1117,13 @@ class Gerber (Geometry):
path = [[current_x, current_y]] # Start new path
continue
- #Parse an aperture.
+ ### Aperture definitions %ADD...
match = self.ad_re.search(gline)
if match:
- self.aperture_parse(match.group(1),match.group(2),match.group(3))
+ self.aperture_parse(match.group(1), match.group(2), match.group(3))
continue
-
- ## G01/2/3* - Interpolation mode change
+ ### G01/2/3* - Interpolation mode change
# Can occur along with coordinates and operation code but
# sometimes by itself (handled here).
# Example: G01*
@@ -739,29 +1132,54 @@ class Gerber (Geometry):
current_interpolation_mode = int(match.group(1))
continue
- ## Tool/aperture change
+ ### Tool/aperture change
# Example: D12*
match = self.tool_re.search(gline)
if match:
current_aperture = match.group(1)
continue
- ## Number format
+ ### Number format
# Example: %FSLAX24Y24*%
# TODO: This is ignoring most of the format. Implement the rest.
match = self.fmt_re.search(gline)
if match:
+ absolute = {'A': True, 'I': False}
self.int_digits = int(match.group(3))
self.frac_digits = int(match.group(4))
continue
- ## Mode (IN/MM)
+ ### Mode (IN/MM)
# Example: %MOIN*%
match = self.mode_re.search(gline)
if match:
self.units = match.group(1)
continue
+ ### Units (G70/1) OBSOLETE
+ match = self.units_re.search(gline)
+ if match:
+ self.units = {'0': 'IN', '1': 'MM'}[match.group(1)]
+ continue
+
+ ### Absolute/relative coordinates G90/1 OBSOLETE
+ match = self.absrel_re.search(gline)
+ if match:
+ absolute = {'0': True, '1': False}[match.group(1)]
+ continue
+
+ #### Ignored lines
+ ## Comments
+ match = self.comm_re.search(gline)
+ if match:
+ continue
+
+ ## EOF
+ match = self.eof_re.search(gline)
+ if match:
+ continue
+
+ ### Line did not match any pattern. Warn user.
print "WARNING: Line ignored (%d):" % line_num, gline
if len(path) > 1:
@@ -821,11 +1239,11 @@ class Gerber (Geometry):
if aperture['type'] == 'P': # Regular polygon
loc = flash['loc']
diam = aperture['diam']
- nVertices = aperture['nVertices']
+ n_vertices = aperture['nVertices']
points = []
- for i in range(0, nVertices):
- x = loc[0] + diam * (cos(2 * pi * i / nVertices))
- y = loc[1] + diam * (sin(2 * pi * i / nVertices))
+ for i in range(0, n_vertices):
+ x = loc[0] + diam * (cos(2 * pi * i / n_vertices))
+ y = loc[1] + diam * (sin(2 * pi * i / n_vertices))
points.append((x, y))
ply = Polygon(points)
if 'rotation' in aperture:
@@ -833,6 +1251,13 @@ class Gerber (Geometry):
self.flash_geometry.append(ply)
continue
+ if aperture['type'] == 'AM': # Aperture Macro
+ loc = flash['loc']
+ flash_geo = aperture['macro'].make_geometry(aperture['modifiers'])
+ flash_geo_final = affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1])
+ self.flash_geometry.append(flash_geo_final)
+ continue
+
print "WARNING: Aperture type %s not implemented" % (aperture['type'])
def create_geometry(self):
@@ -1261,7 +1686,7 @@ class CNCjob(Geometry):
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)
+ tools = filter(lambda i: i in exobj.tools, tools)
print "Tools are:", tools
points = []
@@ -1374,7 +1799,8 @@ class CNCjob(Geometry):
# NOTE: Limited to 1 bracket pair
op = line.find("(")
cl = line.find(")")
- if op > -1 and cl > op:
+ #if op > -1 and cl > op:
+ if cl > op > -1:
#comment = line[op+1:cl]
line = line[:op] + line[(cl+1):]
@@ -1448,7 +1874,6 @@ class CNCjob(Geometry):
"kind": kind})
path = [path[-1]] # Start with the last point of last path.
-
if 'G' in gobj:
current['G'] = int(gobj['G'])
@@ -1677,6 +2102,7 @@ class CNCjob(Geometry):
self.create_geometry()
+
def get_bounds(geometry_set):
xmin = Inf
ymin = Inf
@@ -1776,7 +2202,14 @@ def find_polygon(poly_set, point):
def to_dict(geo):
- output = ''
+ """
+ Makes a Shapely geometry object into serializeable form.
+
+ :param geo: Shapely geometry.
+ :type geo: BaseGeometry
+ :return: Dictionary with serializable form if ``geo`` was
+ BaseGeometry, otherwise returns ``geo``.
+ """
if isinstance(geo, BaseGeometry):
return {
"__class__": "Shply",
@@ -1840,6 +2273,7 @@ def parse_gerber_number(strnumber, frac_digits):
"""
return int(strnumber)*(10**(-frac_digits))
+
def parse_gerber_coords(gstr, int_digits, frac_digits):
"""
Parse Gerber coordinates
diff --git a/defaults.json b/defaults.json
index 433b5513..fec7aea9 100644
--- a/defaults.json
+++ b/defaults.json
@@ -1 +1 @@
-{"geometry_paintoverlap": 0.15, "geometry_plot": true, "excellon_feedrate": 5.0, "gerber_plot": true, "gerber_mergepolys": true, "excellon_drillz": -0.1, "geometry_feedrate": 3.0, "units": "IN", "excellon_travelz": 0.1, "gerber_multicolored": false, "gerber_solid": true, "gerber_isopasses": 1, "excellon_plot": true, "gerber_isotooldia": 0.016, "cncjob_tooldia": 0.016, "geometry_travelz": 0.1, "gerber_cutoutmargin": 0.2, "excellon_solid": false, "geometry_paintmargin": 0.01, "geometry_cutz": -0.002, "geometry_cnctooldia": 0.016, "geometry_painttooldia": 0.0625, "gerber_gaps": "4", "excellon_multicolored": false, "gerber_bboxmargin": 0.0, "cncjob_plot": true, "gerber_cutoutgapsize": 0.15, "gerber_isooverlap": 0.17, "gerber_bboxrounded": false, "geometry_multicolored": false, "gerber_noncoppermargin": 0.0, "geometry_solid": false}
\ No newline at end of file
+{"gerber_cutouttooldia":0.07, "geometry_paintoverlap": 0.15, "geometry_plot": true, "excellon_feedrate": 5.0, "gerber_plot": true, "gerber_mergepolys": true, "excellon_drillz": -0.1, "geometry_feedrate": 3.0, "units": "IN", "excellon_travelz": 0.1, "gerber_multicolored": false, "gerber_solid": true, "gerber_isopasses": 1, "excellon_plot": true, "gerber_isotooldia": 0.016, "cncjob_tooldia": 0.016, "geometry_travelz": 0.1, "gerber_cutoutmargin": 0.2, "excellon_solid": false, "geometry_paintmargin": 0.01, "geometry_cutz": -0.002, "geometry_cnctooldia": 0.016, "geometry_painttooldia": 0.0625, "gerber_gaps": "4", "gerber_bboxmargin": 0.0, "cncjob_plot": true, "gerber_cutoutgapsize": 0.15, "gerber_isooverlap": 0.17, "gerber_bboxrounded": false, "gerber_noncopperrounded": false, "geometry_multicolored": false, "gerber_noncoppermargin": 0.0, "geometry_solid": false}
\ No newline at end of file