Fixes to gerber parser related to aperture macros and aperture definitions allowed characters in names.

This commit is contained in:
Juan Pablo Caram
2014-08-30 12:28:04 -04:00
parent c8b1f22ddb
commit 20c381d510
2 changed files with 333 additions and 308 deletions

639
camlib.py
View File

@@ -6,6 +6,8 @@
# MIT Licence # # MIT Licence #
############################################################ ############################################################
import traceback
from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
from matplotlib.figure import Figure from matplotlib.figure import Figure
import re import re
@@ -31,7 +33,8 @@ import logging
log = logging.getLogger('base2') log = logging.getLogger('base2')
#log.setLevel(logging.DEBUG) #log.setLevel(logging.DEBUG)
log.setLevel(logging.WARNING) #log.setLevel(logging.WARNING)
log.setLevel(logging.INFO)
formatter = logging.Formatter('[%(levelname)s] %(message)s') formatter = logging.Formatter('[%(levelname)s] %(message)s')
handler = logging.StreamHandler() handler = logging.StreamHandler()
handler.setFormatter(formatter) handler.setFormatter(formatter)
@@ -191,6 +194,16 @@ class Geometry(object):
class ApertureMacro: class ApertureMacro:
"""
Syntax of aperture macros.
<AM command>: AM<Aperture macro name>*<Macro content>
<Macro content>: {{<Variable definition>*}{<Primitive>*}}
<Variable definition>: $K=<Arithmetic expression>
<Primitive>: <Primitive code>,<Modifier>{,<Modifier>}|<Comment>
<Modifier>: $M|< Arithmetic expression>
<Comment>: 0 <Text>
"""
## Regular expressions ## Regular expressions
am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$') am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$')
@@ -635,11 +648,11 @@ class Gerber (Geometry):
self.comm_re = re.compile(r'^G0?4(.*)$') self.comm_re = re.compile(r'^G0?4(.*)$')
# AD - Aperture definition # 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-Z_$\.][a-zA-Z0-9_$\.]*)(?:,(.*))?\*%$')
# AM - Aperture Macro # AM - Aperture Macro
# Beginning of macro (Ends with *%): # Beginning of macro (Ends with *%):
self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*') #self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*')
# Tool change # Tool change
# May begin with G54 but that is deprecated # May begin with G54 but that is deprecated
@@ -694,7 +707,7 @@ class Gerber (Geometry):
self.absrel_re = re.compile(r'^G9([01])\*$') self.absrel_re = re.compile(r'^G9([01])\*$')
# Aperture macros # Aperture macros
self.am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$') self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$')
self.am2_re = re.compile(r'(.*)%$') self.am2_re = re.compile(r'(.*)%$')
# TODO: This is bad. # TODO: This is bad.
@@ -915,344 +928,356 @@ class Gerber (Geometry):
#### Parsing starts here #### #### Parsing starts here ####
line_num = 0 line_num = 0
for gline in glines: gline = ""
line_num += 1 try:
for gline in glines:
line_num += 1
### Cleanup ### Cleanup
gline = gline.strip(' \r\n') gline = gline.strip(' \r\n')
### Aperture Macros ### Aperture Macros
# Having this at the beggining will slow things down # Having this at the beggining will slow things down
# but macros can have complicated statements than could # but macros can have complicated statements than could
# be caught by other ptterns. # be caught by other ptterns.
if current_macro is None: # No macro started yet if current_macro is None: # No macro started yet
match = self.am1_re.search(gline) match = self.am1_re.search(gline)
# Start macro if match, else not an AM, carry on. # Start macro if match, else not an AM, carry on.
if match: if match:
current_macro = match.group(1) log.info("Starting macro. Line %d: %s" % (line_num, gline))
self.aperture_macros[current_macro] = ApertureMacro(name=current_macro) current_macro = match.group(1)
if match.group(2): # Append self.aperture_macros[current_macro] = ApertureMacro(name=current_macro)
self.aperture_macros[current_macro].append(match.group(2)) if match.group(2): # Append
if match.group(3): # Finish macro 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
log.info("Macro complete in 1 line.")
continue
else: # Continue macro
log.info("Continuing macro. Line %d." % line_num)
match = self.am2_re.search(gline)
if match: # Finish macro
log.info("End of macro. Line %d." % line_num)
self.aperture_macros[current_macro].append(match.group(1))
#self.aperture_macros[current_macro].parse_content() #self.aperture_macros[current_macro].parse_content()
current_macro = None current_macro = None
continue else: # Append
else: # Continue macro self.aperture_macros[current_macro].append(gline)
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?
# 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:
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:
current_operation_code = int(match.group(4))
# Pen down: add segment
if current_operation_code == 1:
path.append([current_x, current_y])
last_path_aperture = current_aperture
elif current_operation_code == 2:
if len(path) > 1:
## --- BUFFERED ---
if making_region:
geo = Polygon(path)
else:
if last_path_aperture is None:
log.warning("No aperture defined for curent path. (%d)" % line_num)
width = self.apertures[last_path_aperture]["size"]
geo = LineString(path).buffer(width/2)
poly_buffer.append(geo)
path = [[current_x, current_y]] # Start new path
# Flash
elif current_operation_code == 3:
# --- BUFFERED ---
flash = Gerber.create_flash_geometry(Point([current_x, current_y]),
self.apertures[current_aperture])
poly_buffer.append(flash)
continue
### 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:
log.error("Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num)
log.error(gline)
continue continue
if mode is None and current_interpolation_mode not in [2, 3]: ### G01 - Linear interpolation plus flashes
log.error("Found arc without circular interpolation mode defined. (%d)" % line_num) # Operation code (D0x) missing is deprecated... oh well I will support it.
log.error(gline) # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$'
continue match = self.lin_re.search(gline)
elif mode is not None: if match:
current_interpolation_mode = int(mode) # 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.
# Set operation code if provided # Parse coordinates
if d is not None: if match.group(2) is not None:
current_operation_code = int(d) 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)
# Nothing created! Pen Up. # Parse operation code
if current_operation_code == 2: if match.group(4) is not None:
log.warning("Arc with D2. (%d)" % line_num) current_operation_code = int(match.group(4))
if len(path) > 1:
if last_path_aperture is None: # Pen down: add segment
log.warning("No aperture defined for curent path. (%d)" % line_num) if current_operation_code == 1:
path.append([current_x, current_y])
last_path_aperture = current_aperture
elif current_operation_code == 2:
if len(path) > 1:
## --- BUFFERED ---
if making_region:
geo = Polygon(path)
else:
if last_path_aperture is None:
log.warning("No aperture defined for curent path. (%d)" % line_num)
width = self.apertures[last_path_aperture]["size"]
geo = LineString(path).buffer(width/2)
poly_buffer.append(geo)
path = [[current_x, current_y]] # Start new path
# Flash
elif current_operation_code == 3:
# --- BUFFERED --- # --- BUFFERED ---
width = self.apertures[last_path_aperture]["size"] flash = Gerber.create_flash_geometry(Point([current_x, current_y]),
buffered = LineString(path).buffer(width/2) self.apertures[current_aperture])
poly_buffer.append(buffered) poly_buffer.append(flash)
continue
### 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:
log.error("Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num)
log.error(gline)
continue
if mode is None and current_interpolation_mode not in [2, 3]:
log.error("Found arc without circular interpolation mode defined. (%d)" % line_num)
log.error(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:
log.warning("Arc with D2. (%d)" % line_num)
if len(path) > 1:
if last_path_aperture is None:
log.warning("No aperture defined for curent path. (%d)" % line_num)
# --- BUFFERED ---
width = self.apertures[last_path_aperture]["size"]
buffered = LineString(path).buffer(width/2)
poly_buffer.append(buffered)
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:
log.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':
log.warning("Single quadrant arc are not implemented yet. (%d)" % line_num)
### Operation code alone
match = self.opcode_re.search(gline)
if match:
current_operation_code = int(match.group(1))
if current_operation_code == 3:
## --- Buffered ---
flash = Gerber.create_flash_geometry(Point(path[-1]),
self.apertures[current_aperture])
poly_buffer.append(flash)
continue
### 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
### G36* - Begin region
if self.regionon_re.search(gline):
if len(path) > 1:
# Take care of what is left in the path
## --- Buffered ---
width = self.apertures[last_path_aperture]["size"]
geo = LineString(path).buffer(width/2)
poly_buffer.append(geo)
path = [path[-1]]
making_region = True
continue
### G37* - End region
if self.regionoff_re.search(gline):
making_region = False
# Only one path defines region?
# This can happen if D02 happened before G37 and
# is not and error.
if len(path) < 3:
# print "ERROR: Path contains less than 3 points:"
# print path
# print "Line (%d): " % line_num, gline
# path = []
#path = [[current_x, current_y]]
continue
# For regions we may ignore an aperture that is None
# self.regions.append({"polygon": Polygon(path),
# "aperture": last_path_aperture})
# --- Buffered ---
region = Polygon(path)
if not region.is_valid:
region = region.buffer(0)
poly_buffer.append(region)
current_x = x
current_y = y
path = [[current_x, current_y]] # Start new path path = [[current_x, current_y]] # Start new path
continue continue
# Flash should not happen here ### Aperture definitions %ADD...
if current_operation_code == 3: match = self.ad_re.search(gline)
log.error("Trying to flash within arc. (%d)" % line_num) if match:
log.info("Found aperture definition. Line %d: %s" % (line_num, gline))
self.aperture_parse(match.group(1), match.group(2), match.group(3))
continue continue
if quadrant_mode == 'MULTI': ### G01/2/3* - Interpolation mode change
center = [i + current_x, j + current_y] # Can occur along with coordinates and operation code but
radius = sqrt(i**2 + j**2) # sometimes by itself (handled here).
start = arctan2(-j, -i) # Example: G01*
stop = arctan2(-center[1] + y, -center[0] + x) match = self.interp_re.search(gline)
arcdir = [None, None, "cw", "ccw"] if match:
this_arc = arc(center, radius, start, stop, current_interpolation_mode = int(match.group(1))
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 continue
if quadrant_mode == 'SINGLE': ### Tool/aperture change
log.warning("Single quadrant arc are not implemented yet. (%d)" % line_num) # Example: D12*
match = self.tool_re.search(gline)
### Operation code alone if match:
match = self.opcode_re.search(gline) current_aperture = match.group(1)
if match:
current_operation_code = int(match.group(1))
if current_operation_code == 3:
## --- Buffered ---
flash = Gerber.create_flash_geometry(Point(path[-1]),
self.apertures[current_aperture])
poly_buffer.append(flash)
continue
### 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
### G36* - Begin region
if self.regionon_re.search(gline):
if len(path) > 1:
# Take care of what is left in the path
## --- Buffered ---
width = self.apertures[last_path_aperture]["size"]
geo = LineString(path).buffer(width/2)
poly_buffer.append(geo)
path = [path[-1]]
making_region = True
continue
### G37* - End region
if self.regionoff_re.search(gline):
making_region = False
# Only one path defines region?
# This can happen if D02 happened before G37 and
# is not and error.
if len(path) < 3:
# print "ERROR: Path contains less than 3 points:"
# print path
# print "Line (%d): " % line_num, gline
# path = []
#path = [[current_x, current_y]]
continue continue
# For regions we may ignore an aperture that is None ### Polarity change
# self.regions.append({"polygon": Polygon(path), # Example: %LPD*% or %LPC*%
# "aperture": last_path_aperture}) match = self.lpol_re.search(gline)
if match:
if len(path) > 1 and current_polarity != match.group(1):
# --- Buffered --- # --- Buffered ----
region = Polygon(path) width = self.apertures[last_path_aperture]["size"]
if not region.is_valid: geo = LineString(path).buffer(width/2)
region = region.buffer(0) poly_buffer.append(geo)
poly_buffer.append(region)
path = [[current_x, current_y]] # Start new path path = [path[-1]]
continue
### Aperture definitions %ADD...
match = self.ad_re.search(gline)
if match:
self.aperture_parse(match.group(1), match.group(2), match.group(3))
continue
### G01/2/3* - Interpolation mode change # --- Apply buffer ---
# Can occur along with coordinates and operation code but if current_polarity == 'D':
# sometimes by itself (handled here). self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
# Example: G01* else:
match = self.interp_re.search(gline) self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
if match: poly_buffer = []
current_interpolation_mode = int(match.group(1))
continue
### Tool/aperture change current_polarity = match.group(1)
# Example: D12* continue
match = self.tool_re.search(gline)
if match:
current_aperture = match.group(1)
continue
### Polarity change ### Number format
# Example: %LPD*% or %LPC*% # Example: %FSLAX24Y24*%
match = self.lpol_re.search(gline) # TODO: This is ignoring most of the format. Implement the rest.
if match: match = self.fmt_re.search(gline)
if len(path) > 1 and current_polarity != match.group(1): if match:
absolute = {'A': True, 'I': False}
self.int_digits = int(match.group(3))
self.frac_digits = int(match.group(4))
continue
# --- Buffered ---- ### Mode (IN/MM)
width = self.apertures[last_path_aperture]["size"] # Example: %MOIN*%
geo = LineString(path).buffer(width/2) match = self.mode_re.search(gline)
poly_buffer.append(geo) if match:
self.units = match.group(1)
continue
path = [path[-1]] ### Units (G70/1) OBSOLETE
match = self.units_re.search(gline)
if match:
self.units = {'0': 'IN', '1': 'MM'}[match.group(1)]
continue
# --- Apply buffer --- ### Absolute/relative coordinates G90/1 OBSOLETE
if current_polarity == 'D': match = self.absrel_re.search(gline)
self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer)) if match:
else: absolute = {'0': True, '1': False}[match.group(1)]
self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer)) continue
poly_buffer = []
current_polarity = match.group(1) #### Ignored lines
continue ## Comments
match = self.comm_re.search(gline)
if match:
continue
### Number format ## EOF
# Example: %FSLAX24Y24*% match = self.eof_re.search(gline)
# TODO: This is ignoring most of the format. Implement the rest. if match:
match = self.fmt_re.search(gline) continue
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) ### Line did not match any pattern. Warn user.
# Example: %MOIN*% log.warning("Line ignored (%d): %s" % (line_num, gline))
match = self.mode_re.search(gline)
if match:
self.units = match.group(1)
continue
### Units (G70/1) OBSOLETE if len(path) > 1:
match = self.units_re.search(gline) # EOF, create shapely LineString if something still in path
if match:
self.units = {'0': 'IN', '1': 'MM'}[match.group(1)]
continue
### Absolute/relative coordinates G90/1 OBSOLETE ## --- Buffered ---
match = self.absrel_re.search(gline) width = self.apertures[last_path_aperture]["size"]
if match: geo = LineString(path).buffer(width/2)
absolute = {'0': True, '1': False}[match.group(1)] poly_buffer.append(geo)
continue
#### Ignored lines # --- Apply buffer ---
## Comments if current_polarity == 'D':
match = self.comm_re.search(gline) self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
if match: else:
continue self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
## EOF except Exception, err:
match = self.eof_re.search(gline) #print traceback.format_exc()
if match: log.error("PARSING FAILED. Line %d: %s" % (line_num, gline))
continue raise
### Line did not match any pattern. Warn user.
log.warning("Line ignored (%d): %s" % (line_num, gline))
if len(path) > 1:
# EOF, create shapely LineString if something still in path
## --- Buffered ---
width = self.apertures[last_path_aperture]["size"]
geo = LineString(path).buffer(width/2)
poly_buffer.append(geo)
# --- Apply buffer ---
if current_polarity == 'D':
self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
else:
self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
@staticmethod @staticmethod
def create_flash_geometry(location, aperture): def create_flash_geometry(location, aperture):

View File

@@ -1 +1 @@
[{"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/FlatCam_Drilling_Test/FlatCam_Drilling_Test.drl"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Excellon_Planck/X-Y CONTROLLER - Drill Data - Through Hole.drl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/7V-PSU/7V PSU.GTL"}, {"kind": "cncjob", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/output.gcode"}, {"kind": "project", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/project_copy.fcproj"}, {"kind": "project", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/a_project.fcproj"}, {"kind": "project", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/FlatCAM_TestProject.fcproj"}, {"kind": "cncjob", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/FlatCAM_TestGCode.gcode"}, {"kind": "cncjob", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/CBS-B_Cu_ISOLATION_GCODE.ngc"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/FlatCAM_TestDrill.drl"}] [{"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/WindMills - Bottom Copper 2.gbr"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Example1_copper_bottom.gbr"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/7V-PSU.zip"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Gerbers/AVR_Transistor_Tester_copper_top.GTL"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/XGerber/do-kotle.Bot"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/FlatCam_Drilling_Test/FlatCam_Drilling_Test.drl"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Excellon_Planck/X-Y CONTROLLER - Drill Data - Through Hole.drl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/7V-PSU/7V PSU.GTL"}, {"kind": "cncjob", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/output.gcode"}, {"kind": "project", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/project_copy.fcproj"}]