|
|
@@ -1,7 +1,7 @@
|
|
|
# ############################################################
|
|
|
# FlatCAM: 2D Post-processing for Manufacturing #
|
|
|
# http://flatcam.org #
|
|
|
-# File Author: Marius Adrina Stanciu (c) #
|
|
|
+# File Author: Marius Adrian Stanciu (c) #
|
|
|
# Date: 12/11/2019 #
|
|
|
# MIT Licence #
|
|
|
# ############################################################
|
|
|
@@ -17,7 +17,7 @@ from copy import deepcopy
|
|
|
import sys
|
|
|
|
|
|
from shapely.ops import cascaded_union, unary_union
|
|
|
-from shapely.geometry import Polygon, MultiPolygon, LineString, Point
|
|
|
+from shapely.geometry import Polygon, MultiPolygon, LineString, Point, MultiLineString
|
|
|
import shapely.affinity as affinity
|
|
|
from shapely.geometry import box as shply_box
|
|
|
|
|
|
@@ -62,7 +62,54 @@ class HPGL2(Geometry):
|
|
|
self.coord_mm_factor = 0.040
|
|
|
|
|
|
# store the file units here:
|
|
|
- self.units = self.app.defaults['gerber_def_units']
|
|
|
+ self.units = 'MM'
|
|
|
+
|
|
|
+ # storage for the tools
|
|
|
+ self.tools = dict()
|
|
|
+
|
|
|
+ self.default_data = dict()
|
|
|
+ self.default_data.update({
|
|
|
+ "name": '_ncc',
|
|
|
+ "plot": self.app.defaults["geometry_plot"],
|
|
|
+ "cutz": self.app.defaults["geometry_cutz"],
|
|
|
+ "vtipdia": self.app.defaults["geometry_vtipdia"],
|
|
|
+ "vtipangle": self.app.defaults["geometry_vtipangle"],
|
|
|
+ "travelz": self.app.defaults["geometry_travelz"],
|
|
|
+ "feedrate": self.app.defaults["geometry_feedrate"],
|
|
|
+ "feedrate_z": self.app.defaults["geometry_feedrate_z"],
|
|
|
+ "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
|
|
|
+ "dwell": self.app.defaults["geometry_dwell"],
|
|
|
+ "dwelltime": self.app.defaults["geometry_dwelltime"],
|
|
|
+ "multidepth": self.app.defaults["geometry_multidepth"],
|
|
|
+ "ppname_g": self.app.defaults["geometry_ppname_g"],
|
|
|
+ "depthperpass": self.app.defaults["geometry_depthperpass"],
|
|
|
+ "extracut": self.app.defaults["geometry_extracut"],
|
|
|
+ "extracut_length": self.app.defaults["geometry_extracut_length"],
|
|
|
+ "toolchange": self.app.defaults["geometry_toolchange"],
|
|
|
+ "toolchangez": self.app.defaults["geometry_toolchangez"],
|
|
|
+ "endz": self.app.defaults["geometry_endz"],
|
|
|
+ "spindlespeed": self.app.defaults["geometry_spindlespeed"],
|
|
|
+ "toolchangexy": self.app.defaults["geometry_toolchangexy"],
|
|
|
+ "startz": self.app.defaults["geometry_startz"],
|
|
|
+
|
|
|
+ "tooldia": self.app.defaults["tools_painttooldia"],
|
|
|
+ "paintmargin": self.app.defaults["tools_paintmargin"],
|
|
|
+ "paintmethod": self.app.defaults["tools_paintmethod"],
|
|
|
+ "selectmethod": self.app.defaults["tools_selectmethod"],
|
|
|
+ "pathconnect": self.app.defaults["tools_pathconnect"],
|
|
|
+ "paintcontour": self.app.defaults["tools_paintcontour"],
|
|
|
+ "paintoverlap": self.app.defaults["tools_paintoverlap"],
|
|
|
+
|
|
|
+ "nccoverlap": self.app.defaults["tools_nccoverlap"],
|
|
|
+ "nccmargin": self.app.defaults["tools_nccmargin"],
|
|
|
+ "nccmethod": self.app.defaults["tools_nccmethod"],
|
|
|
+ "nccconnect": self.app.defaults["tools_nccconnect"],
|
|
|
+ "ncccontour": self.app.defaults["tools_ncccontour"],
|
|
|
+ "nccrest": self.app.defaults["tools_nccrest"]
|
|
|
+ })
|
|
|
+
|
|
|
+ # flag to be set True when tool is detected
|
|
|
+ self.tool_detected = False
|
|
|
|
|
|
# will store the geometry's as solids
|
|
|
self.solid_geometry = None
|
|
|
@@ -82,17 +129,17 @@ class HPGL2(Geometry):
|
|
|
# comment
|
|
|
self.comment_re = re.compile(r"^CO\s*[\"']([a-zA-Z0-9\s]*)[\"'];?$")
|
|
|
# absolute move to x, y
|
|
|
- self.abs_move_re = re.compile(r"^PA\s*(-?\d+\.\d+?),?\s*(-?\d+\.\d+?)*;?$")
|
|
|
+ self.abs_move_re = re.compile(r"^PA\s*(-?\d+\.?\d+?),?\s*(-?\d+\.?\d+?)*;?$")
|
|
|
# relative move to x, y
|
|
|
self.rel_move_re = re.compile(r"^PR\s*(-?\d+\.\d+?),?\s*(-?\d+\.\d+?)*;?$")
|
|
|
# pen position
|
|
|
self.pen_re = re.compile(r"^(P[U|D]);?$")
|
|
|
# Initialize
|
|
|
- self.mode_re = re.compile(r'^(IN);?$')
|
|
|
+ self.initialize_re = re.compile(r'^(IN);?$')
|
|
|
+
|
|
|
# select pen
|
|
|
self.sp_re = re.compile(r'SP(\d);?$')
|
|
|
|
|
|
-
|
|
|
self.fmt_re_alt = re.compile(r'%FS([LTD])?([AI])X(\d)(\d)Y\d\d\*MO(IN|MM)\*%$')
|
|
|
self.fmt_re_orcad = re.compile(r'(G\d+)*\**%FS([LTD])?([AI]).*X(\d)(\d)Y\d\d\*%$')
|
|
|
|
|
|
@@ -108,12 +155,6 @@ class HPGL2(Geometry):
|
|
|
self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X([+-]?\d+))?(?=.*Y([+-]?\d+))' +
|
|
|
'?(?=.*I([+-]?\d+))?(?=.*J([+-]?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$')
|
|
|
|
|
|
- # G01/2/3 Occurring without coordinates
|
|
|
- self.interp_re = re.compile(r'^(?:G0?([123]))\*')
|
|
|
-
|
|
|
- # Single G74 or multi G75 quadrant for circular interpolation
|
|
|
- self.quad_re = re.compile(r'^G7([45]).*\*$')
|
|
|
-
|
|
|
# Absolute/Relative G90/1 (OBSOLETE)
|
|
|
self.absrel_re = re.compile(r'^G9([01])\*$')
|
|
|
|
|
|
@@ -121,25 +162,13 @@ class HPGL2(Geometry):
|
|
|
# in a Gerber file (normal or obsolete ones)
|
|
|
self.conversion_done = False
|
|
|
|
|
|
- self.use_buffer_for_union = self.app.defaults["gerber_use_buffer_for_union"]
|
|
|
+ self.in_header = None
|
|
|
|
|
|
- def parse_file(self, filename, follow=False):
|
|
|
+ def parse_file(self, filename):
|
|
|
"""
|
|
|
- Calls Gerber.parse_lines() with generator of lines
|
|
|
- read from the given file. Will split the lines if multiple
|
|
|
- statements are found in a single original line.
|
|
|
-
|
|
|
- The following line is split into two::
|
|
|
-
|
|
|
- G54D11*G36*
|
|
|
-
|
|
|
- First is ``G54D11*`` and seconds is ``G36*``.
|
|
|
|
|
|
- :param filename: Gerber file to parse.
|
|
|
+ :param filename: HPGL2 file to parse.
|
|
|
:type filename: str
|
|
|
- :param follow: If true, will not create polygons, just lines
|
|
|
- following the gerber path.
|
|
|
- :type follow: bool
|
|
|
:return: None
|
|
|
"""
|
|
|
|
|
|
@@ -148,10 +177,9 @@ class HPGL2(Geometry):
|
|
|
|
|
|
def parse_lines(self, glines):
|
|
|
"""
|
|
|
- Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
|
|
|
- ``self.flashes``, ``self.regions`` and ``self.units``.
|
|
|
+ Main HPGL2 parser.
|
|
|
|
|
|
- :param glines: Gerber code as list of strings, each element being
|
|
|
+ :param glines: HPGL2 code as list of strings, each element being
|
|
|
one line of the source file.
|
|
|
:type glines: list
|
|
|
:return: None
|
|
|
@@ -159,33 +187,9 @@ class HPGL2(Geometry):
|
|
|
"""
|
|
|
|
|
|
# Coordinates of the current path, each is [x, y]
|
|
|
- path = []
|
|
|
+ path = list()
|
|
|
|
|
|
- # this is for temporary storage of solid geometry until it is added to poly_buffer
|
|
|
- geo_s = None
|
|
|
-
|
|
|
- # this is for temporary storage of follow geometry until it is added to follow_buffer
|
|
|
- geo_f = None
|
|
|
-
|
|
|
- # Polygons are stored here until there is a change in polarity.
|
|
|
- # Only then they are combined via cascaded_union and added or
|
|
|
- # subtracted from solid_geometry. This is ~100 times faster than
|
|
|
- # applying a union for every new polygon.
|
|
|
- poly_buffer = []
|
|
|
-
|
|
|
- # store here the follow geometry
|
|
|
- follow_buffer = []
|
|
|
-
|
|
|
- last_path_aperture = None
|
|
|
- current_aperture = None
|
|
|
-
|
|
|
- # 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
|
|
|
+ geo_buffer = []
|
|
|
|
|
|
# Current coordinates
|
|
|
current_x = None
|
|
|
@@ -193,31 +197,17 @@ class HPGL2(Geometry):
|
|
|
previous_x = None
|
|
|
previous_y = None
|
|
|
|
|
|
- current_d = None
|
|
|
-
|
|
|
- # Absolute or Relative/Incremental coordinates
|
|
|
- # Not implemented
|
|
|
- absolute = True
|
|
|
+ # store the pen (tool) status
|
|
|
+ pen_status = 'up'
|
|
|
|
|
|
- # How to interpret circular interpolation: SINGLE or MULTI
|
|
|
- quadrant_mode = None
|
|
|
-
|
|
|
- # Indicates we are parsing an aperture macro
|
|
|
- current_macro = None
|
|
|
-
|
|
|
- # Indicates the current polarity: D-Dark, C-Clear
|
|
|
- current_polarity = 'D'
|
|
|
-
|
|
|
- # If a region is being defined
|
|
|
- making_region = False
|
|
|
+ # store the current tool here
|
|
|
+ current_tool = None
|
|
|
|
|
|
# ### Parsing starts here ## ##
|
|
|
line_num = 0
|
|
|
gline = ""
|
|
|
|
|
|
- s_tol = float(self.app.defaults["gerber_simp_tolerance"])
|
|
|
-
|
|
|
- self.app.inform.emit('%s %d %s.' % (_("Gerber processing. Parsing"), len(glines), _("lines")))
|
|
|
+ self.app.inform.emit('%s %d %s.' % (_("HPGL2 processing. Parsing"), len(glines), _("lines")))
|
|
|
try:
|
|
|
for gline in glines:
|
|
|
if self.app.abort_flag:
|
|
|
@@ -235,466 +225,293 @@ class HPGL2(Geometry):
|
|
|
# Ignored lines #####
|
|
|
# Comments #####
|
|
|
# ###################
|
|
|
- match = self.comm_re.search(gline)
|
|
|
- if match:
|
|
|
- continue
|
|
|
-
|
|
|
- # ## Mode (IN/MM)
|
|
|
- # Example: %MOIN*%
|
|
|
- match = self.mode_re.search(gline)
|
|
|
+ match = self.comment_re.search(gline)
|
|
|
if match:
|
|
|
- self.units = match.group(1)
|
|
|
- log.debug("Gerber units found = %s" % self.units)
|
|
|
- # Changed for issue #80
|
|
|
- # self.convert_units(match.group(1))
|
|
|
- self.conversion_done = True
|
|
|
+ log.debug(str(match.group(1)))
|
|
|
continue
|
|
|
|
|
|
- # ############################################################# ##
|
|
|
- # Absolute/relative coordinates G90/1 OBSOLETE ######## ##
|
|
|
- # ##################################################### ##
|
|
|
+ # #####################################################
|
|
|
+ # Absolute/relative coordinates G90/1 OBSOLETE ########
|
|
|
+ # #####################################################
|
|
|
match = self.absrel_re.search(gline)
|
|
|
if match:
|
|
|
absolute = {'0': "Absolute", '1': "Relative"}[match.group(1)]
|
|
|
log.warning("Gerber obsolete coordinates type found = %s (Absolute or Relative) " % absolute)
|
|
|
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)
|
|
|
+ # search for the initialization
|
|
|
+ match = self.initialize_re.search(gline)
|
|
|
if match:
|
|
|
- # Parse coordinates
|
|
|
- if match.group(2) is not None:
|
|
|
- linear_x = parse_number(match.group(2),
|
|
|
- self.int_digits, self.frac_digits, self.gerber_zeros)
|
|
|
- current_x = linear_x
|
|
|
- else:
|
|
|
- linear_x = current_x
|
|
|
- if match.group(3) is not None:
|
|
|
- linear_y = parse_number(match.group(3),
|
|
|
- self.int_digits, self.frac_digits, self.gerber_zeros)
|
|
|
- current_y = linear_y
|
|
|
- else:
|
|
|
- linear_y = current_y
|
|
|
-
|
|
|
- # 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:
|
|
|
- # if linear_x or linear_y are None, ignore those
|
|
|
- if current_x is not None and current_y is not None:
|
|
|
- # only add the point if it's a new one otherwise skip it (harder to process)
|
|
|
- if path[-1] != [current_x, current_y]:
|
|
|
- path.append([current_x, current_y])
|
|
|
-
|
|
|
- if making_region is False:
|
|
|
- # if the aperture is rectangle then add a rectangular shape having as parameters the
|
|
|
- # coordinates of the start and end point and also the width and height
|
|
|
- # of the 'R' aperture
|
|
|
- try:
|
|
|
- if self.apertures[current_aperture]["type"] == 'R':
|
|
|
- width = self.apertures[current_aperture]['width']
|
|
|
- height = self.apertures[current_aperture]['height']
|
|
|
- minx = min(path[0][0], path[1][0]) - width / 2
|
|
|
- maxx = max(path[0][0], path[1][0]) + width / 2
|
|
|
- miny = min(path[0][1], path[1][1]) - height / 2
|
|
|
- maxy = max(path[0][1], path[1][1]) + height / 2
|
|
|
- log.debug("Coords: %s - %s - %s - %s" % (minx, miny, maxx, maxy))
|
|
|
-
|
|
|
- geo_dict = dict()
|
|
|
- geo_f = Point([current_x, current_y])
|
|
|
- follow_buffer.append(geo_f)
|
|
|
- geo_dict['follow'] = geo_f
|
|
|
-
|
|
|
- geo_s = shply_box(minx, miny, maxx, maxy)
|
|
|
- if self.app.defaults['gerber_simplification']:
|
|
|
- poly_buffer.append(geo_s.simplify(s_tol))
|
|
|
- else:
|
|
|
- poly_buffer.append(geo_s)
|
|
|
-
|
|
|
- if self.is_lpc is True:
|
|
|
- geo_dict['clear'] = geo_s
|
|
|
- else:
|
|
|
- geo_dict['solid'] = geo_s
|
|
|
-
|
|
|
- if current_aperture not in self.apertures:
|
|
|
- self.apertures[current_aperture] = dict()
|
|
|
- if 'geometry' not in self.apertures[current_aperture]:
|
|
|
- self.apertures[current_aperture]['geometry'] = []
|
|
|
- self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
|
|
|
- except Exception as e:
|
|
|
- pass
|
|
|
- last_path_aperture = current_aperture
|
|
|
- # we do this for the case that a region is done without having defined any aperture
|
|
|
- if last_path_aperture is None:
|
|
|
- if '0' not in self.apertures:
|
|
|
- self.apertures['0'] = {}
|
|
|
- self.apertures['0']['type'] = 'REG'
|
|
|
- self.apertures['0']['size'] = 0.0
|
|
|
- self.apertures['0']['geometry'] = []
|
|
|
- last_path_aperture = '0'
|
|
|
- else:
|
|
|
- self.app.inform.emit('[WARNING] %s: %s' %
|
|
|
- (_("Coordinates missing, line ignored"), str(gline)))
|
|
|
- self.app.inform.emit('[WARNING_NOTCL] %s' %
|
|
|
- _("GERBER file might be CORRUPT. Check the file !!!"))
|
|
|
- elif current_operation_code == 2:
|
|
|
- if len(path) > 1:
|
|
|
- geo_s = None
|
|
|
-
|
|
|
- geo_dict = dict()
|
|
|
- # --- BUFFERED ---
|
|
|
- # this treats the case when we are storing geometry as paths only
|
|
|
- if making_region:
|
|
|
- # we do this for the case that a region is done without having defined any aperture
|
|
|
- if last_path_aperture is None:
|
|
|
- if '0' not in self.apertures:
|
|
|
- self.apertures['0'] = {}
|
|
|
- self.apertures['0']['type'] = 'REG'
|
|
|
- self.apertures['0']['size'] = 0.0
|
|
|
- self.apertures['0']['geometry'] = []
|
|
|
- last_path_aperture = '0'
|
|
|
- geo_f = Polygon()
|
|
|
- else:
|
|
|
- geo_f = LineString(path)
|
|
|
-
|
|
|
- try:
|
|
|
- if self.apertures[last_path_aperture]["type"] != 'R':
|
|
|
- if not geo_f.is_empty:
|
|
|
- follow_buffer.append(geo_f)
|
|
|
- geo_dict['follow'] = geo_f
|
|
|
- except Exception as e:
|
|
|
- log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
|
|
|
- if not geo_f.is_empty:
|
|
|
- follow_buffer.append(geo_f)
|
|
|
- geo_dict['follow'] = geo_f
|
|
|
-
|
|
|
- # this treats the case when we are storing geometry as solids
|
|
|
- if making_region:
|
|
|
- # we do this for the case that a region is done without having defined any aperture
|
|
|
- if last_path_aperture is None:
|
|
|
- if '0' not in self.apertures:
|
|
|
- self.apertures['0'] = {}
|
|
|
- self.apertures['0']['type'] = 'REG'
|
|
|
- self.apertures['0']['size'] = 0.0
|
|
|
- self.apertures['0']['geometry'] = []
|
|
|
- last_path_aperture = '0'
|
|
|
-
|
|
|
- try:
|
|
|
- geo_s = Polygon(path)
|
|
|
- except ValueError:
|
|
|
- log.warning("Problem %s %s" % (gline, line_num))
|
|
|
- self.app.inform.emit('[ERROR] %s: %s' %
|
|
|
- (_("Region does not have enough points. "
|
|
|
- "File will be processed but there are parser errors. "
|
|
|
- "Line number"), str(line_num)))
|
|
|
- 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"] # TODO: WARNING this should fail!
|
|
|
- geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
|
|
|
-
|
|
|
- try:
|
|
|
- if self.apertures[last_path_aperture]["type"] != 'R':
|
|
|
- if not geo_s.is_empty:
|
|
|
- if self.app.defaults['gerber_simplification']:
|
|
|
- poly_buffer.append(geo_s.simplify(s_tol))
|
|
|
- else:
|
|
|
- poly_buffer.append(geo_s)
|
|
|
-
|
|
|
- if self.is_lpc is True:
|
|
|
- geo_dict['clear'] = geo_s
|
|
|
- else:
|
|
|
- geo_dict['solid'] = geo_s
|
|
|
- except Exception as e:
|
|
|
- log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
|
|
|
- if self.app.defaults['gerber_simplification']:
|
|
|
- poly_buffer.append(geo_s.simplify(s_tol))
|
|
|
- else:
|
|
|
- poly_buffer.append(geo_s)
|
|
|
-
|
|
|
- if self.is_lpc is True:
|
|
|
- geo_dict['clear'] = geo_s
|
|
|
- else:
|
|
|
- geo_dict['solid'] = geo_s
|
|
|
-
|
|
|
- if last_path_aperture not in self.apertures:
|
|
|
- self.apertures[last_path_aperture] = dict()
|
|
|
- if 'geometry' not in self.apertures[last_path_aperture]:
|
|
|
- self.apertures[last_path_aperture]['geometry'] = []
|
|
|
- self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
|
|
|
-
|
|
|
- # if linear_x or linear_y are None, ignore those
|
|
|
- if linear_x is not None and linear_y is not None:
|
|
|
- path = [[linear_x, linear_y]] # Start new path
|
|
|
- else:
|
|
|
- self.app.inform.emit('[WARNING] %s: %s' %
|
|
|
- (_("Coordinates missing, line ignored"), str(gline)))
|
|
|
- self.app.inform.emit('[WARNING_NOTCL] %s' %
|
|
|
- _("GERBER file might be CORRUPT. Check the file !!!"))
|
|
|
-
|
|
|
- # maybe those lines are not exactly needed but it is easier to read the program as those coordinates
|
|
|
- # are used in case that circular interpolation is encountered within the Gerber file
|
|
|
- current_x = linear_x
|
|
|
- current_y = linear_y
|
|
|
-
|
|
|
- # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
|
|
|
+ self.in_header = False
|
|
|
continue
|
|
|
|
|
|
- # ## G02/3 - Circular interpolation
|
|
|
- # 2-clockwise, 3-counterclockwise
|
|
|
- # Ex. format: G03 X0 Y50 I-50 J0 where the X, Y coords are the coords of the End Point
|
|
|
- match = self.circ_re.search(gline)
|
|
|
- if match:
|
|
|
- arcdir = [None, None, "cw", "ccw"]
|
|
|
-
|
|
|
- mode, circular_x, circular_y, i, j, d = match.groups()
|
|
|
-
|
|
|
- try:
|
|
|
- circular_x = parse_number(circular_x,
|
|
|
- self.int_digits, self.frac_digits, self.gerber_zeros)
|
|
|
- except Exception as e:
|
|
|
- circular_x = current_x
|
|
|
-
|
|
|
- try:
|
|
|
- circular_y = parse_number(circular_y,
|
|
|
- self.int_digits, self.frac_digits, self.gerber_zeros)
|
|
|
- except Exception as e:
|
|
|
- circular_y = current_y
|
|
|
-
|
|
|
- # According to Gerber specification i and j are not modal, which means that when i or j are missing,
|
|
|
- # they are to be interpreted as being zero
|
|
|
- try:
|
|
|
- i = parse_number(i, self.int_digits, self.frac_digits, self.gerber_zeros)
|
|
|
- except Exception as e:
|
|
|
- i = 0
|
|
|
-
|
|
|
- try:
|
|
|
- j = parse_number(j, self.int_digits, self.frac_digits, self.gerber_zeros)
|
|
|
- except Exception as e:
|
|
|
- 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:
|
|
|
- geo_dict = dict()
|
|
|
-
|
|
|
- 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"]
|
|
|
-
|
|
|
- # this treats the case when we are storing geometry as paths
|
|
|
- geo_f = LineString(path)
|
|
|
- if not geo_f.is_empty:
|
|
|
- follow_buffer.append(geo_f)
|
|
|
- geo_dict['follow'] = geo_f
|
|
|
-
|
|
|
- # this treats the case when we are storing geometry as solids
|
|
|
- buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
|
|
|
- if not buffered.is_empty:
|
|
|
- if self.app.defaults['gerber_simplification']:
|
|
|
- poly_buffer.append(buffered.simplify(s_tol))
|
|
|
- else:
|
|
|
- poly_buffer.append(buffered)
|
|
|
-
|
|
|
- if self.is_lpc is True:
|
|
|
- geo_dict['clear'] = buffered
|
|
|
- else:
|
|
|
- geo_dict['solid'] = buffered
|
|
|
-
|
|
|
- if last_path_aperture not in self.apertures:
|
|
|
- self.apertures[last_path_aperture] = dict()
|
|
|
- if 'geometry' not in self.apertures[last_path_aperture]:
|
|
|
- self.apertures[last_path_aperture]['geometry'] = []
|
|
|
- self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
|
|
|
-
|
|
|
- current_x = circular_x
|
|
|
- current_y = circular_y
|
|
|
- path = [[current_x, current_y]] # Start new path
|
|
|
+ if self.in_header is False:
|
|
|
+ # tools detection
|
|
|
+ match = self.sp_re.search(gline)
|
|
|
+ if match:
|
|
|
+ tool = match.group(1)
|
|
|
+ # self.tools[tool] = dict()
|
|
|
+ self.tools.update({
|
|
|
+ tool: {
|
|
|
+ 'tooldia': float('%.*f' %
|
|
|
+ (
|
|
|
+ self.decimals,
|
|
|
+ float(self.app.defaults['geometry_cnctooldia'])
|
|
|
+ )
|
|
|
+ ),
|
|
|
+ 'offset': 'Path',
|
|
|
+ 'offset_value': 0.0,
|
|
|
+ 'type': 'Iso',
|
|
|
+ 'tool_type': 'C1',
|
|
|
+ 'data': deepcopy(self.default_data),
|
|
|
+ 'solid_geometry': list()
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ if current_tool:
|
|
|
+ if path:
|
|
|
+ geo = LineString(path)
|
|
|
+ self.tools[current_tool]['solid_geometry'].append(geo)
|
|
|
+ geo_buffer.append(geo)
|
|
|
+ path[:] = []
|
|
|
+
|
|
|
+ current_tool = tool
|
|
|
continue
|
|
|
|
|
|
- # Flash should not happen here
|
|
|
- if current_operation_code == 3:
|
|
|
- log.error("Trying to flash within arc. (%d)" % line_num)
|
|
|
+ # pen status detection
|
|
|
+ match = self.pen_re.search(gline)
|
|
|
+ if match:
|
|
|
+ pen_status = {'PU': 'up', 'PD': 'down'}[match.group(1)]
|
|
|
continue
|
|
|
|
|
|
- if quadrant_mode == 'MULTI':
|
|
|
- center = [i + current_x, j + current_y]
|
|
|
- radius = np.sqrt(i ** 2 + j ** 2)
|
|
|
- start = np.arctan2(-j, -i) # Start angle
|
|
|
- # Numerical errors might prevent start == stop therefore
|
|
|
- # we check ahead of time. This should result in a
|
|
|
- # 360 degree arc.
|
|
|
- if current_x == circular_x and current_y == circular_y:
|
|
|
- stop = start
|
|
|
+ # linear move
|
|
|
+ match = self.abs_move_re.search(gline)
|
|
|
+ if match:
|
|
|
+ # Parse coordinates
|
|
|
+ if match.group(1) is not None:
|
|
|
+ linear_x = parse_number(match.group(1))
|
|
|
+ current_x = linear_x
|
|
|
else:
|
|
|
- stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
|
|
|
-
|
|
|
- this_arc = arc(center, radius, start, stop,
|
|
|
- arcdir[current_interpolation_mode],
|
|
|
- self.steps_per_circle)
|
|
|
+ linear_x = current_x
|
|
|
|
|
|
- # The last point in the computed arc can have
|
|
|
- # numerical errors. The exact final point is the
|
|
|
- # specified (x, y). Replace.
|
|
|
- this_arc[-1] = (circular_x, circular_y)
|
|
|
-
|
|
|
- # Last point in path is current point
|
|
|
- # current_x = this_arc[-1][0]
|
|
|
- # current_y = this_arc[-1][1]
|
|
|
- current_x, current_y = circular_x, circular_y
|
|
|
-
|
|
|
- # Append
|
|
|
- path += this_arc
|
|
|
- last_path_aperture = current_aperture
|
|
|
+ if match.group(2) is not None:
|
|
|
+ linear_y = parse_number(match.group(2))
|
|
|
+ current_y = linear_y
|
|
|
+ else:
|
|
|
+ linear_y = current_y
|
|
|
+
|
|
|
+ # Pen down: add segment
|
|
|
+ if pen_status == 'down':
|
|
|
+ # if linear_x or linear_y are None, ignore those
|
|
|
+ if current_x is not None and current_y is not None:
|
|
|
+ # only add the point if it's a new one otherwise skip it (harder to process)
|
|
|
+ if path[-1] != [current_x, current_y]:
|
|
|
+ path.append([current_x, current_y])
|
|
|
+ else:
|
|
|
+ self.app.inform.emit('[WARNING] %s: %s' %
|
|
|
+ (_("Coordinates missing, line ignored"), str(gline)))
|
|
|
+
|
|
|
+ elif pen_status == 'up':
|
|
|
+ if len(path) > 1:
|
|
|
+ geo = LineString(path)
|
|
|
+ self.tools[current_tool]['solid_geometry'].append(geo)
|
|
|
+ geo_buffer.append(geo)
|
|
|
+
|
|
|
+ # if linear_x or linear_y are None, ignore those
|
|
|
+ if linear_x is not None and linear_y is not None:
|
|
|
+ path = [[linear_x, linear_y]] # Start new path
|
|
|
+ else:
|
|
|
+ self.app.inform.emit('[WARNING] %s: %s' %
|
|
|
+ (_("Coordinates missing, line ignored"), str(gline)))
|
|
|
|
|
|
+ # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
|
|
|
continue
|
|
|
|
|
|
- if quadrant_mode == 'SINGLE':
|
|
|
-
|
|
|
- center_candidates = [
|
|
|
- [i + current_x, j + current_y],
|
|
|
- [-i + current_x, j + current_y],
|
|
|
- [i + current_x, -j + current_y],
|
|
|
- [-i + current_x, -j + current_y]
|
|
|
- ]
|
|
|
-
|
|
|
- valid = False
|
|
|
- log.debug("I: %f J: %f" % (i, j))
|
|
|
- for center in center_candidates:
|
|
|
- radius = np.sqrt(i ** 2 + j ** 2)
|
|
|
-
|
|
|
- # Make sure radius to start is the same as radius to end.
|
|
|
- radius2 = np.sqrt((center[0] - circular_x) ** 2 + (center[1] - circular_y) ** 2)
|
|
|
- if radius2 < radius * 0.95 or radius2 > radius * 1.05:
|
|
|
- continue # Not a valid center.
|
|
|
-
|
|
|
- # Correct i and j and continue as with multi-quadrant.
|
|
|
- i = center[0] - current_x
|
|
|
- j = center[1] - current_y
|
|
|
-
|
|
|
- start = np.arctan2(-j, -i) # Start angle
|
|
|
- stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
|
|
|
- angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode]))
|
|
|
- log.debug("ARC START: %f, %f CENTER: %f, %f STOP: %f, %f" %
|
|
|
- (current_x, current_y, center[0], center[1], circular_x, circular_y))
|
|
|
- log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" %
|
|
|
- (start * 180 / np.pi, stop * 180 / np.pi, arcdir[current_interpolation_mode],
|
|
|
- angle * 180 / np.pi, np.pi / 2 * 180 / np.pi, angle <= (np.pi + 1e-6) / 2))
|
|
|
-
|
|
|
- if angle <= (np.pi + 1e-6) / 2:
|
|
|
- log.debug("########## ACCEPTING ARC ############")
|
|
|
- this_arc = arc(center, radius, start, stop,
|
|
|
- arcdir[current_interpolation_mode],
|
|
|
- self.steps_per_circle)
|
|
|
-
|
|
|
- # Replace with exact values
|
|
|
- this_arc[-1] = (circular_x, circular_y)
|
|
|
-
|
|
|
- # current_x = this_arc[-1][0]
|
|
|
- # current_y = this_arc[-1][1]
|
|
|
- current_x, current_y = circular_x, circular_y
|
|
|
-
|
|
|
- path += this_arc
|
|
|
- last_path_aperture = current_aperture
|
|
|
- valid = True
|
|
|
- break
|
|
|
-
|
|
|
- if valid:
|
|
|
- continue
|
|
|
- else:
|
|
|
- log.warning("Invalid arc in line %d." % line_num)
|
|
|
-
|
|
|
+ # ## Circular interpolation
|
|
|
+ # -clockwise,
|
|
|
+ # -counterclockwise
|
|
|
+ match = self.circ_re.search(gline)
|
|
|
+ # if match:
|
|
|
+ # arcdir = [None, None, "cw", "ccw"]
|
|
|
+ #
|
|
|
+ # mode, circular_x, circular_y, i, j, d = match.groups()
|
|
|
+ #
|
|
|
+ # try:
|
|
|
+ # circular_x = parse_number(circular_x)
|
|
|
+ # except Exception as e:
|
|
|
+ # circular_x = current_x
|
|
|
+ #
|
|
|
+ # try:
|
|
|
+ # circular_y = parse_number(circular_y)
|
|
|
+ # except Exception as e:
|
|
|
+ # circular_y = current_y
|
|
|
+ #
|
|
|
+ # try:
|
|
|
+ # i = parse_number(i)
|
|
|
+ # except Exception as e:
|
|
|
+ # i = 0
|
|
|
+ #
|
|
|
+ # try:
|
|
|
+ # j = parse_number(j)
|
|
|
+ # except Exception as e:
|
|
|
+ # j = 0
|
|
|
+ #
|
|
|
+ # 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:
|
|
|
+ # geo_dict = dict()
|
|
|
+ #
|
|
|
+ # 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"]
|
|
|
+ #
|
|
|
+ # # this treats the case when we are storing geometry as paths
|
|
|
+ # geo_f = LineString(path)
|
|
|
+ # if not geo_f.is_empty:
|
|
|
+ # geo_dict['follow'] = geo_f
|
|
|
+ #
|
|
|
+ # # this treats the case when we are storing geometry as solids
|
|
|
+ # buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
|
|
|
+ #
|
|
|
+ # if last_path_aperture not in self.apertures:
|
|
|
+ # self.apertures[last_path_aperture] = dict()
|
|
|
+ # if 'geometry' not in self.apertures[last_path_aperture]:
|
|
|
+ # self.apertures[last_path_aperture]['geometry'] = []
|
|
|
+ # self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
|
|
|
+ #
|
|
|
+ # current_x = circular_x
|
|
|
+ # current_y = circular_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 = np.sqrt(i ** 2 + j ** 2)
|
|
|
+ # start = np.arctan2(-j, -i) # Start angle
|
|
|
+ # # Numerical errors might prevent start == stop therefore
|
|
|
+ # # we check ahead of time. This should result in a
|
|
|
+ # # 360 degree arc.
|
|
|
+ # if current_x == circular_x and current_y == circular_y:
|
|
|
+ # stop = start
|
|
|
+ # else:
|
|
|
+ # stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
|
|
|
+ #
|
|
|
+ # this_arc = arc(center, radius, start, stop,
|
|
|
+ # arcdir[current_interpolation_mode],
|
|
|
+ # self.steps_per_circle)
|
|
|
+ #
|
|
|
+ # # The last point in the computed arc can have
|
|
|
+ # # numerical errors. The exact final point is the
|
|
|
+ # # specified (x, y). Replace.
|
|
|
+ # this_arc[-1] = (circular_x, circular_y)
|
|
|
+ #
|
|
|
+ # # Last point in path is current point
|
|
|
+ # # current_x = this_arc[-1][0]
|
|
|
+ # # current_y = this_arc[-1][1]
|
|
|
+ # current_x, current_y = circular_x, circular_y
|
|
|
+ #
|
|
|
+ # # Append
|
|
|
+ # path += this_arc
|
|
|
+ # last_path_aperture = current_aperture
|
|
|
+ #
|
|
|
+ # continue
|
|
|
+ #
|
|
|
+ # if quadrant_mode == 'SINGLE':
|
|
|
+ #
|
|
|
+ # center_candidates = [
|
|
|
+ # [i + current_x, j + current_y],
|
|
|
+ # [-i + current_x, j + current_y],
|
|
|
+ # [i + current_x, -j + current_y],
|
|
|
+ # [-i + current_x, -j + current_y]
|
|
|
+ # ]
|
|
|
+ #
|
|
|
+ # valid = False
|
|
|
+ # log.debug("I: %f J: %f" % (i, j))
|
|
|
+ # for center in center_candidates:
|
|
|
+ # radius = np.sqrt(i ** 2 + j ** 2)
|
|
|
+ #
|
|
|
+ # # Make sure radius to start is the same as radius to end.
|
|
|
+ # radius2 = np.sqrt((center[0] - circular_x) ** 2 + (center[1] - circular_y) ** 2)
|
|
|
+ # if radius2 < radius * 0.95 or radius2 > radius * 1.05:
|
|
|
+ # continue # Not a valid center.
|
|
|
+ #
|
|
|
+ # # Correct i and j and continue as with multi-quadrant.
|
|
|
+ # i = center[0] - current_x
|
|
|
+ # j = center[1] - current_y
|
|
|
+ #
|
|
|
+ # start = np.arctan2(-j, -i) # Start angle
|
|
|
+ # stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
|
|
|
+ # angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode]))
|
|
|
+ # log.debug("ARC START: %f, %f CENTER: %f, %f STOP: %f, %f" %
|
|
|
+ # (current_x, current_y, center[0], center[1], circular_x, circular_y))
|
|
|
+ # log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" %
|
|
|
+ # (start * 180 / np.pi, stop * 180 / np.pi, arcdir[current_interpolation_mode],
|
|
|
+ # angle * 180 / np.pi, np.pi / 2 * 180 / np.pi, angle <= (np.pi + 1e-6) / 2))
|
|
|
+ #
|
|
|
+ # if angle <= (np.pi + 1e-6) / 2:
|
|
|
+ # log.debug("########## ACCEPTING ARC ############")
|
|
|
+ # this_arc = arc(center, radius, start, stop,
|
|
|
+ # arcdir[current_interpolation_mode],
|
|
|
+ # self.steps_per_circle)
|
|
|
+ #
|
|
|
+ # # Replace with exact values
|
|
|
+ # this_arc[-1] = (circular_x, circular_y)
|
|
|
+ #
|
|
|
+ # # current_x = this_arc[-1][0]
|
|
|
+ # # current_y = this_arc[-1][1]
|
|
|
+ # current_x, current_y = circular_x, circular_y
|
|
|
+ #
|
|
|
+ # path += this_arc
|
|
|
+ # last_path_aperture = current_aperture
|
|
|
+ # valid = True
|
|
|
+ # break
|
|
|
+ #
|
|
|
+ # if valid:
|
|
|
+ # continue
|
|
|
+ # else:
|
|
|
+ # log.warning("Invalid arc in line %d." % line_num)
|
|
|
|
|
|
# ## Line did not match any pattern. Warn user.
|
|
|
log.warning("Line ignored (%d): %s" % (line_num, gline))
|
|
|
|
|
|
- # --- Apply buffer ---
|
|
|
- # this treats the case when we are storing geometry as paths
|
|
|
- self.follow_geometry = follow_buffer
|
|
|
-
|
|
|
- # this treats the case when we are storing geometry as solids
|
|
|
-
|
|
|
- if len(poly_buffer) == 0 and len(self.solid_geometry) == 0:
|
|
|
- log.error("Object is not Gerber file or empty. Aborting Object creation.")
|
|
|
+ if len(geo_buffer) == 0 and len(self.solid_geometry) == 0:
|
|
|
+ log.error("Object is not HPGL2 file or empty. Aborting Object creation.")
|
|
|
return 'fail'
|
|
|
|
|
|
- log.warning("Joining %d polygons." % len(poly_buffer))
|
|
|
- self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(poly_buffer)))
|
|
|
-
|
|
|
- if self.use_buffer_for_union:
|
|
|
- log.debug("Union by buffer...")
|
|
|
+ log.warning("Joining %d polygons." % len(geo_buffer))
|
|
|
+ self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(geo_buffer)))
|
|
|
|
|
|
- new_poly = MultiPolygon(poly_buffer)
|
|
|
- if self.app.defaults["gerber_buffering"] == 'full':
|
|
|
- new_poly = new_poly.buffer(0.00000001)
|
|
|
- new_poly = new_poly.buffer(-0.00000001)
|
|
|
- log.warning("Union(buffer) done.")
|
|
|
- else:
|
|
|
- log.debug("Union by union()...")
|
|
|
- new_poly = cascaded_union(poly_buffer)
|
|
|
- new_poly = new_poly.buffer(0, int(self.steps_per_circle / 4))
|
|
|
- log.warning("Union done.")
|
|
|
-
|
|
|
- if current_polarity == 'D':
|
|
|
- self.app.inform.emit('%s' % _("Gerber processing. Applying Gerber polarity."))
|
|
|
- if new_poly.is_valid:
|
|
|
- self.solid_geometry = self.solid_geometry.union(new_poly)
|
|
|
- else:
|
|
|
- # I do this so whenever the parsed geometry of the file is not valid (intersections) it is still
|
|
|
- # loaded. Instead of applying a union I add to a list of polygons.
|
|
|
- final_poly = []
|
|
|
- try:
|
|
|
- for poly in new_poly:
|
|
|
- final_poly.append(poly)
|
|
|
- except TypeError:
|
|
|
- final_poly.append(new_poly)
|
|
|
-
|
|
|
- try:
|
|
|
- for poly in self.solid_geometry:
|
|
|
- final_poly.append(poly)
|
|
|
- except TypeError:
|
|
|
- final_poly.append(self.solid_geometry)
|
|
|
-
|
|
|
- self.solid_geometry = final_poly
|
|
|
-
|
|
|
- else:
|
|
|
- self.solid_geometry = self.solid_geometry.difference(new_poly)
|
|
|
+ new_poly = unary_union(geo_buffer)
|
|
|
+ self.solid_geometry = new_poly
|
|
|
|
|
|
- # init this for the following operations
|
|
|
- self.conversion_done = False
|
|
|
except Exception as err:
|
|
|
ex_type, ex, tb = sys.exc_info()
|
|
|
traceback.print_tb(tb)
|
|
|
# print traceback.format_exc()
|
|
|
|
|
|
- log.error("Gerber PARSING FAILED. Line %d: %s" % (line_num, gline))
|
|
|
+ log.error("HPGL2 PARSING FAILED. Line %d: %s" % (line_num, gline))
|
|
|
|
|
|
- loc = '%s #%d %s: %s\n' % (_("Gerber Line"), line_num, _("Gerber Line Content"), gline) + repr(err)
|
|
|
- self.app.inform.emit('[ERROR] %s\n%s:' %
|
|
|
- (_("Gerber Parser ERROR"), loc))
|
|
|
+ loc = '%s #%d %s: %s\n' % (_("HPGL2 Line"), line_num, _("HPGL2 Line Content"), gline) + repr(err)
|
|
|
+ self.app.inform.emit('[ERROR] %s\n%s:' % (_("HPGL2 Parser ERROR"), loc))
|
|
|
|
|
|
def create_geometry(self):
|
|
|
"""
|
|
|
@@ -1245,5 +1062,5 @@ def parse_number(strnumber):
|
|
|
:rtype: float
|
|
|
"""
|
|
|
|
|
|
- return float(strnumber) * 40.0 # in milimeters
|
|
|
+ return float(strnumber) / 40.0 # in milimeters
|
|
|
|