ParseExcellon.py 70 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599
  1. # ########################################################## ##
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 2/5/2014 #
  6. # MIT Licence #
  7. # ########################################################## ##
  8. from camlib import Geometry, grace
  9. import shapely.affinity as affinity
  10. from shapely.geometry import Point, LineString
  11. import numpy as np
  12. import re
  13. import logging
  14. import traceback
  15. from copy import deepcopy
  16. # import AppTranslation as fcTranslate
  17. import gettext
  18. import builtins
  19. if '_' not in builtins.__dict__:
  20. _ = gettext.gettext
  21. log = logging.getLogger('base')
  22. class Excellon(Geometry):
  23. """
  24. Here it is done all the Excellon parsing.
  25. *ATTRIBUTES*
  26. * ``tools`` (dict): The key is the tool name and the value is
  27. a dictionary specifying the tool:
  28. ================ ====================================
  29. Key Value
  30. ================ ====================================
  31. tooldia Diameter of the tool
  32. drills List that store the Shapely Points for drill points
  33. slots List that store the Shapely Points for slots. Each is a tuple: (start_point, stop_point)
  34. data dictionary which holds the options for each tool
  35. solid_geometry Geometry list for each tool
  36. ================ ====================================
  37. """
  38. defaults = {
  39. "zeros": "L",
  40. "excellon_format_upper_mm": '3',
  41. "excellon_format_lower_mm": '3',
  42. "excellon_format_upper_in": '2',
  43. "excellon_format_lower_in": '4',
  44. "excellon_units": 'INCH',
  45. "geo_steps_per_circle": '64'
  46. }
  47. def __init__(self, zeros=None, excellon_format_upper_mm=None, excellon_format_lower_mm=None,
  48. excellon_format_upper_in=None, excellon_format_lower_in=None, excellon_units=None,
  49. geo_steps_per_circle=None):
  50. """
  51. The constructor takes no parameters.
  52. :return: Excellon object.
  53. :rtype: Excellon
  54. """
  55. self.decimals = self.app.decimals
  56. if geo_steps_per_circle is None:
  57. geo_steps_per_circle = int(Excellon.defaults['geo_steps_per_circle'])
  58. self.geo_steps_per_circle = int(geo_steps_per_circle)
  59. Geometry.__init__(self, geo_steps_per_circle=int(geo_steps_per_circle))
  60. # dictionary to store tools, see above for description
  61. self.tools = {}
  62. self.source_file = ''
  63. # it serve to flag if a start routing or a stop routing was encountered
  64. # if a stop is encounter and this flag is still 0 (so there is no stop for a previous start) issue error
  65. self.routing_flag = 1
  66. self.match_routing_start = None
  67. self.match_routing_stop = None
  68. # ## IN|MM -> Units are inherited from Geometry
  69. self.units = self.app.defaults['units']
  70. self.units_found = self.app.defaults['units']
  71. # Trailing "T" or leading "L" (default)
  72. # self.zeros = "T"
  73. self.zeros = zeros or self.defaults["zeros"]
  74. self.zeros_found = deepcopy(self.zeros)
  75. # this will serve as a default if the Excellon file has no info regarding of tool diameters (this info may be
  76. # in another file like for PCB WIzard ECAD software
  77. self.toolless_diam = 1.0
  78. # signal that the Excellon file has no tool diameter informations and the tools have bogus (random) diameter
  79. self.diameterless = False
  80. # Excellon format
  81. self.excellon_format_upper_in = excellon_format_upper_in or self.defaults["excellon_format_upper_in"]
  82. self.excellon_format_lower_in = excellon_format_lower_in or self.defaults["excellon_format_lower_in"]
  83. self.excellon_format_upper_mm = excellon_format_upper_mm or self.defaults["excellon_format_upper_mm"]
  84. self.excellon_format_lower_mm = excellon_format_lower_mm or self.defaults["excellon_format_lower_mm"]
  85. self.excellon_units = excellon_units or self.defaults["excellon_units"]
  86. self.excellon_units_found = None
  87. # detected Excellon format is stored here:
  88. self.excellon_format = None
  89. # Attributes to be included in serialization
  90. # Always append to it because it carries contents
  91. # from Geometry.
  92. self.ser_attrs += ['zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm',
  93. 'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'source_file']
  94. # ### Patterns ####
  95. # Regex basics:
  96. # ^ - beginning
  97. # $ - end
  98. # *: 0 or more, +: 1 or more, ?: 0 or 1
  99. # M48 - Beginning of Part Program Header
  100. self.hbegin_re = re.compile(r'^M48$')
  101. # ;HEADER - Beginning of Allegro Program Header
  102. self.allegro_hbegin_re = re.compile(r'\;\s*(HEADER)')
  103. # M95 or % - End of Part Program Header
  104. # NOTE: % has different meaning in the body
  105. self.hend_re = re.compile(r'^(?:M95|%)$')
  106. # FMAT Excellon format
  107. # Ignored in the parser
  108. # self.fmat_re = re.compile(r'^FMAT,([12])$')
  109. # Uunits and possible Excellon zeros and possible Excellon format
  110. # INCH uses 6 digits
  111. # METRIC uses 5/6
  112. self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?,?(\d*\.\d+)?.*$')
  113. # Tool definition/parameters (?= is look-ahead
  114. # NOTE: This might be an overkill!
  115. # self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' +
  116. # r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
  117. # r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
  118. # r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
  119. self.toolset_re = re.compile(r'^T(\d+)(?=.*C,?(\d*\.?\d*))?' +
  120. r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
  121. r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
  122. r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
  123. self.detect_gcode_re = re.compile(r'^G2([01])$')
  124. # Tool select
  125. # Can have additional data after tool number but
  126. # is ignored if present in the header.
  127. # Warning: This will match toolset_re too.
  128. # self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))')
  129. self.toolsel_re = re.compile(r'^T(\d+)')
  130. # Headerless toolset
  131. # self.toolset_hl_re = re.compile(r'^T(\d+)(?=.*C(\d*\.?\d*))')
  132. self.toolset_hl_re = re.compile(r'^T(\d+)(?:.?C(\d+\.?\d*))?')
  133. # Comment
  134. self.comm_re = re.compile(r'^;(.*)$')
  135. # Absolute/Incremental G90/G91
  136. self.absinc_re = re.compile(r'^G9([01])$')
  137. # Modes of operation
  138. # 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill
  139. self.modes_re = re.compile(r'^G0([012345])')
  140. # Measuring mode
  141. # 1-metric, 2-inch
  142. self.meas_re = re.compile(r'^M7([12])$')
  143. # Coordinates
  144. # self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
  145. # self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
  146. coordsperiod_re_string = r'(?=.*X([-\+]?\d*\.\d*))?(?=.*Y([-\+]?\d*\.\d*))?[XY]'
  147. self.coordsperiod_re = re.compile(coordsperiod_re_string)
  148. coordsnoperiod_re_string = r'(?!.*\.)(?=.*X([-\+]?\d*))?(?=.*Y([-\+]?\d*))?[XY]'
  149. self.coordsnoperiod_re = re.compile(coordsnoperiod_re_string)
  150. # Slots parsing
  151. slots_re_string = r'^([^G]+)G85(.*)$'
  152. self.slots_re = re.compile(slots_re_string)
  153. # R - Repeat hole (# times, X offset, Y offset)
  154. self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X([-\+]?\d*\.?\d*))?(?:Y([-\+]?\d*\.?\d*))?$')
  155. # Various stop/pause commands
  156. self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')
  157. # Allegro Excellon format support
  158. self.tool_units_re = re.compile(r'(\;\s*Holesize \d+.\s*\=\s*(\d+.\d+).*(MILS|MM))')
  159. # Altium Excellon format support
  160. # it's a comment like this: ";FILE_FORMAT=2:5"
  161. self.altium_format = re.compile(r'^;\s*(?:FILE_FORMAT)?(?:Format)?[=|:]\s*(\d+)[:|.](\d+).*$')
  162. # Parse coordinates
  163. self.leadingzeros_re = re.compile(r'^[-\+]?(0*)(\d*)')
  164. # Repeating command
  165. self.repeat_re = re.compile(r'R(\d+)')
  166. def parse_file(self, filename=None, file_obj=None):
  167. """
  168. Reads the specified file as array of lines as passes it to ``parse_lines()``.
  169. :param filename: The file to be read and parsed.
  170. :param file_obj:
  171. :type filename: str
  172. :return: None
  173. """
  174. if file_obj:
  175. estr = file_obj
  176. else:
  177. if filename is None:
  178. return "fail"
  179. efile = open(filename, 'r')
  180. estr = efile.readlines()
  181. efile.close()
  182. try:
  183. self.parse_lines(estr)
  184. except Exception:
  185. return "fail"
  186. def parse_lines(self, elines):
  187. """
  188. Main Excellon parser.
  189. :param elines: List of strings, each being a line of Excellon code.
  190. :type elines: list
  191. :return: None
  192. """
  193. # State variables
  194. current_tool = ""
  195. in_header = False
  196. headerless = False
  197. current_x = None
  198. current_y = None
  199. slot_current_x = None
  200. slot_current_y = None
  201. name_tool = 0
  202. allegro_warning = False
  203. line_units_found = False
  204. repeating_x = 0
  205. repeating_y = 0
  206. repeat = 0
  207. line_units = ''
  208. # ## Parsing starts here ## ##
  209. line_num = 0 # Line number
  210. eline = ""
  211. try:
  212. for eline in elines:
  213. if self.app.abort_flag:
  214. # graceful abort requested by the user
  215. raise grace
  216. line_num += 1
  217. # log.debug("%3d %s" % (line_num, str(eline)))
  218. self.source_file += eline
  219. # Cleanup lines
  220. eline = eline.strip(' \r\n')
  221. # Excellon files and Gcode share some extensions therefore if we detect G20 or G21 it's GCODe
  222. # and we need to exit from here
  223. if self.detect_gcode_re.search(eline):
  224. log.warning("This is GCODE mark: %s" % eline)
  225. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_('This is GCODE mark'), eline))
  226. return
  227. # Header Begin (M48) #
  228. if self.hbegin_re.search(eline):
  229. in_header = True
  230. headerless = False
  231. log.warning("Found start of the header: %s" % eline)
  232. continue
  233. # Allegro Header Begin (;HEADER) #
  234. if self.allegro_hbegin_re.search(eline):
  235. in_header = True
  236. allegro_warning = True
  237. log.warning("Found ALLEGRO start of the header: %s" % eline)
  238. continue
  239. # Search for Header End #
  240. # Since there might be comments in the header that include header end char (% or M95)
  241. # we ignore the lines starting with ';' that contains such header end chars because it is not a
  242. # real header end.
  243. if self.comm_re.search(eline):
  244. match = self.tool_units_re.search(eline)
  245. if match:
  246. if line_units_found is False:
  247. line_units_found = True
  248. line_units = match.group(3)
  249. self.convert_units({"MILS": "IN", "MM": "MM"}[line_units])
  250. log.warning("Type of Allegro UNITS found inline in comments: %s" % line_units)
  251. if match.group(2):
  252. name_tool += 1
  253. # ---------- add a TOOL ------------ #
  254. if name_tool not in self.tools:
  255. self.tools[name_tool] = {}
  256. if line_units == 'MILS':
  257. spec = {
  258. 'tooldia': (float(match.group(2)) / 1000)
  259. }
  260. self.tools[name_tool]['tooldia'] = (float(match.group(2)) / 1000)
  261. log.debug("Tool definition: %d %s" % (name_tool, spec))
  262. else:
  263. spec = {
  264. 'tooldia': float(match.group(2))
  265. }
  266. self.tools[name_tool]['tooldia'] = float(match.group(2))
  267. log.debug("Tool definition: %d %s" % (name_tool, spec))
  268. spec['solid_geometry'] = []
  269. continue
  270. # search for Altium Excellon Format / Sprint Layout who is included as a comment
  271. match = self.altium_format.search(eline)
  272. if match:
  273. self.excellon_format_upper_mm = match.group(1)
  274. self.excellon_format_lower_mm = match.group(2)
  275. self.excellon_format_upper_in = match.group(1)
  276. self.excellon_format_lower_in = match.group(2)
  277. log.warning("Excellon format preset found in comments: %s:%s" %
  278. (match.group(1), match.group(2)))
  279. continue
  280. else:
  281. log.warning("Line ignored, it's a comment: %s" % eline)
  282. else:
  283. if self.hend_re.search(eline):
  284. if in_header is False or bool(self.tools) is False:
  285. log.warning("Found end of the header but there is no header: %s" % eline)
  286. log.warning("The only useful data in header are tools, units and format.")
  287. log.warning("Therefore we will create units and format based on defaults.")
  288. headerless = True
  289. try:
  290. self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.excellon_units])
  291. except Exception as e:
  292. log.warning("Units could not be converted: %s" % str(e))
  293. in_header = False
  294. # for Allegro type of Excellons we reset name_tool variable so we can reuse it for toolchange
  295. if allegro_warning is True:
  296. name_tool = 0
  297. log.warning("Found end of the header: %s" % eline)
  298. '''
  299. In case that the units were not found in the header, we have two choices:
  300. - one is to use the default value in the App Preferences
  301. - the other is to make an evaluation based on a threshold
  302. we process here the self.tools list and make a list with tools with diameter less or equal
  303. with 0.1 and a list with tools with value greater than 0.1, 0.1 being the threshold value.
  304. Most tools in Excellon are greater than 0.1mm therefore if most of the tools are under this
  305. value it is safe to assume that the units are in INCH
  306. '''
  307. greater_tools = set()
  308. lower_tools = set()
  309. if not self.excellon_units_found and self.tools:
  310. for tool in self.tools:
  311. tool_dia = float(self.tools[tool]['tooldia'])
  312. lower_tools.add(tool_dia) if tool_dia <= 0.1 else greater_tools.add(tool_dia)
  313. assumed_units = "IN" if len(lower_tools) > len(greater_tools) else "MM"
  314. self.units = assumed_units
  315. continue
  316. # ## Alternative units format M71/M72
  317. # Supposed to be just in the body (yes, the body)
  318. # but some put it in the header (PADS for example).
  319. # Will detect anywhere. Occurrence will change the
  320. # object's units.
  321. match = self.meas_re.match(eline)
  322. if match:
  323. self.units = {"1": "MM", "2": "IN"}[match.group(1)]
  324. # Modified for issue #80
  325. log.debug("ALternative M71/M72 units found, before conversion: %s" % self.units)
  326. self.convert_units(self.units)
  327. log.debug("ALternative M71/M72 units found, after conversion: %s" % self.units)
  328. if self.units == 'MM':
  329. log.warning("Excellon format preset is: %s:%s" %
  330. (str(self.excellon_format_upper_mm), str(self.excellon_format_lower_mm)))
  331. else:
  332. log.warning("Excellon format preset is: %s:%s" %
  333. (str(self.excellon_format_upper_in), str(self.excellon_format_lower_in)))
  334. continue
  335. # ### Body ####
  336. if not in_header:
  337. # ## Tool change ###
  338. match = self.toolsel_re.search(eline)
  339. if match:
  340. current_tool = int(match.group(1))
  341. log.debug("Tool change: %s" % current_tool)
  342. if bool(headerless):
  343. match = self.toolset_hl_re.search(eline)
  344. if match:
  345. name = int(match.group(1))
  346. try:
  347. diam = float(match.group(2))
  348. except Exception:
  349. # it's possible that tool definition has only tool number and no diameter info
  350. # (those could be in another file like PCB Wizard do)
  351. # then match.group(2) = None and float(None) will create the exception
  352. # the below construction is so each tool will have a slightly different diameter
  353. # starting with a default value, to allow Excellon editing after that
  354. self.diameterless = True
  355. self.app.inform.emit('[WARNING] %s%s %s' %
  356. (_("No tool diameter info's. See shell.\n"
  357. "A tool change event: T"),
  358. str(current_tool),
  359. _("was found but the Excellon file "
  360. "have no informations regarding the tool "
  361. "diameters therefore the application will try to load it "
  362. "by using some 'fake' diameters.\n"
  363. "The user needs to edit the resulting Excellon object and "
  364. "change the diameters to reflect the real diameters.")
  365. )
  366. )
  367. if self.excellon_units == 'MM':
  368. diam = self.toolless_diam + (int(current_tool) - 1) / 100
  369. else:
  370. diam = (self.toolless_diam + (int(current_tool) - 1) / 100) / 25.4
  371. # ---------- add a TOOL ------------ #
  372. spec = {"tooldia": diam, 'solid_geometry': []}
  373. if name not in self.tools:
  374. self.tools[name] = {}
  375. self.tools[name]['tooldia'] = diam
  376. self.tools[name]['solid_geometry'] = []
  377. log.debug("Tool definition out of header: %s %s" % (name, spec))
  378. continue
  379. # ## Allegro Type Tool change ###
  380. if allegro_warning is True:
  381. match = self.absinc_re.search(eline)
  382. match1 = self.stop_re.search(eline)
  383. if match or match1:
  384. name_tool += 1
  385. current_tool = name_tool
  386. log.debug("Tool change for Allegro type of Excellon: %d" % current_tool)
  387. continue
  388. # ## Slots parsing for drilled slots (contain G85)
  389. # a Excellon drilled slot line may look like this:
  390. # X01125Y0022244G85Y0027756
  391. match = self.slots_re.search(eline)
  392. if match:
  393. # signal that there are milling slots operations
  394. self.defaults['excellon_drills'] = False
  395. # the slot start coordinates group is to the left of G85 command (group(1) )
  396. # the slot stop coordinates group is to the right of G85 command (group(2) )
  397. start_coords_match = match.group(1)
  398. stop_coords_match = match.group(2)
  399. # Slot coordinates without period # ##
  400. # get the coordinates for slot start and for slot stop into variables
  401. start_coords_noperiod = self.coordsnoperiod_re.search(start_coords_match)
  402. stop_coords_noperiod = self.coordsnoperiod_re.search(stop_coords_match)
  403. if start_coords_noperiod:
  404. try:
  405. slot_start_x = self.parse_number(start_coords_noperiod.group(1))
  406. slot_current_x = slot_start_x
  407. except TypeError:
  408. slot_start_x = slot_current_x
  409. except Exception:
  410. return
  411. try:
  412. slot_start_y = self.parse_number(start_coords_noperiod.group(2))
  413. slot_current_y = slot_start_y
  414. except TypeError:
  415. slot_start_y = slot_current_y
  416. except Exception:
  417. return
  418. try:
  419. slot_stop_x = self.parse_number(stop_coords_noperiod.group(1))
  420. slot_current_x = slot_stop_x
  421. except TypeError:
  422. slot_stop_x = slot_current_x
  423. except Exception:
  424. return
  425. try:
  426. slot_stop_y = self.parse_number(stop_coords_noperiod.group(2))
  427. slot_current_y = slot_stop_y
  428. except TypeError:
  429. slot_stop_y = slot_current_y
  430. except Exception:
  431. return
  432. if (slot_start_x is None or slot_start_y is None or
  433. slot_stop_x is None or slot_stop_y is None):
  434. log.error("Slots are missing some or all coordinates.")
  435. continue
  436. # we have a slot
  437. log.debug('Parsed a slot with coordinates: ' + str([slot_start_x,
  438. slot_start_y, slot_stop_x,
  439. slot_stop_y]))
  440. # store current tool diameter as slot diameter
  441. slot_dia = 0.05
  442. try:
  443. slot_dia = float(self.tools[current_tool]['tooldia'])
  444. except Exception:
  445. pass
  446. log.debug(
  447. 'Milling/Drilling slot with tool %s, diam=%f' % (
  448. current_tool,
  449. slot_dia
  450. )
  451. )
  452. # ---------- add a slot ------------ #
  453. slot = (
  454. Point(slot_start_x, slot_start_y),
  455. Point(slot_stop_x, slot_stop_y)
  456. )
  457. if current_tool not in self.tools:
  458. self.tools[current_tool] = {}
  459. if 'slots' in self.tools[current_tool]:
  460. self.tools[current_tool]['slots'].append(slot)
  461. else:
  462. self.tools[current_tool]['slots'] = [slot]
  463. continue
  464. # Slot coordinates with period: Use literally. ###
  465. # get the coordinates for slot start and for slot stop into variables
  466. start_coords_period = self.coordsperiod_re.search(start_coords_match)
  467. stop_coords_period = self.coordsperiod_re.search(stop_coords_match)
  468. if start_coords_period:
  469. try:
  470. slot_start_x = float(start_coords_period.group(1))
  471. slot_current_x = slot_start_x
  472. except TypeError:
  473. slot_start_x = slot_current_x
  474. except Exception:
  475. return
  476. try:
  477. slot_start_y = float(start_coords_period.group(2))
  478. slot_current_y = slot_start_y
  479. except TypeError:
  480. slot_start_y = slot_current_y
  481. except Exception:
  482. return
  483. try:
  484. slot_stop_x = float(stop_coords_period.group(1))
  485. slot_current_x = slot_stop_x
  486. except TypeError:
  487. slot_stop_x = slot_current_x
  488. except Exception:
  489. return
  490. try:
  491. slot_stop_y = float(stop_coords_period.group(2))
  492. slot_current_y = slot_stop_y
  493. except TypeError:
  494. slot_stop_y = slot_current_y
  495. except Exception:
  496. return
  497. if (slot_start_x is None or slot_start_y is None or
  498. slot_stop_x is None or slot_stop_y is None):
  499. log.error("Slots are missing some or all coordinates.")
  500. continue
  501. # we have a slot
  502. log.debug('Parsed a slot with coordinates: ' + str([slot_start_x,
  503. slot_start_y, slot_stop_x,
  504. slot_stop_y]))
  505. # store current tool diameter as slot diameter
  506. slot_dia = 0.05
  507. try:
  508. slot_dia = float(self.tools[current_tool]['tooldia'])
  509. except Exception:
  510. pass
  511. log.debug(
  512. 'Milling/Drilling slot with tool %s, diam=%f' % (
  513. current_tool,
  514. slot_dia
  515. )
  516. )
  517. # ---------- add a Slot ------------ #
  518. slot = (
  519. Point(slot_start_x, slot_start_y),
  520. Point(slot_stop_x, slot_stop_y)
  521. )
  522. if current_tool not in self.tools:
  523. self.tools[current_tool] = {}
  524. if 'slots' in self.tools[current_tool]:
  525. self.tools[current_tool]['slots'].append(slot)
  526. else:
  527. self.tools[current_tool]['slots'] = [slot]
  528. continue
  529. # ## Coordinates without period # ##
  530. match = self.coordsnoperiod_re.search(eline)
  531. if match:
  532. matchr = self.repeat_re.search(eline)
  533. if matchr: # if we have a repeat command
  534. repeat = int(matchr.group(1))
  535. if match.group(1):
  536. repeating_x = self.parse_number(match.group(1))
  537. else:
  538. repeating_x = 0
  539. if match.group(2):
  540. repeating_y = self.parse_number(match.group(2))
  541. else:
  542. repeating_y = 0
  543. coordx = current_x
  544. coordy = current_y
  545. while repeat > 0:
  546. if repeating_x:
  547. coordx += repeating_x
  548. if repeating_y:
  549. coordy += repeating_y
  550. # ---------- add a Drill ------------ #
  551. if current_tool not in self.tools:
  552. self.tools[current_tool] = {}
  553. if 'drills' in self.tools[current_tool]:
  554. self.tools[current_tool]['drills'].append(Point((coordx, coordy)))
  555. else:
  556. self.tools[current_tool]['drills'] = [Point((coordx, coordy))]
  557. repeat -= 1
  558. current_x = coordx
  559. current_y = coordy
  560. continue
  561. else: # those are normal coordinates
  562. try:
  563. x = self.parse_number(match.group(1))
  564. current_x = x
  565. except TypeError:
  566. x = current_x
  567. except Exception:
  568. return
  569. try:
  570. y = self.parse_number(match.group(2))
  571. current_y = y
  572. except TypeError:
  573. y = current_y
  574. except Exception:
  575. return
  576. if x is None or y is None:
  577. log.error("Missing coordinates")
  578. continue
  579. # ## Excellon Routing parse
  580. if len(re.findall("G00", eline)) > 0:
  581. self.match_routing_start = 'G00'
  582. # signal that there are milling slots operations
  583. self.defaults['excellon_drills'] = False
  584. self.routing_flag = 0
  585. slot_start_x = x
  586. slot_start_y = y
  587. continue
  588. if self.routing_flag == 0:
  589. if len(re.findall("G01", eline)) > 0:
  590. self.match_routing_stop = 'G01'
  591. # signal that there are milling slots operations
  592. self.defaults['excellon_drills'] = False
  593. self.routing_flag = 1
  594. slot_stop_x = x
  595. slot_stop_y = y
  596. # ---------- add a Slot ------------ #
  597. slot = (
  598. Point(slot_start_x, slot_start_y),
  599. Point(slot_stop_x, slot_stop_y)
  600. )
  601. if current_tool not in self.tools:
  602. self.tools[current_tool] = {}
  603. if 'slots' in self.tools[current_tool]:
  604. self.tools[current_tool]['slots'].append(slot)
  605. else:
  606. self.tools[current_tool]['slots'] = [slot]
  607. continue
  608. if self.match_routing_start is None and self.match_routing_stop is None:
  609. # signal that there are drill operations
  610. self.defaults['excellon_drills'] = True
  611. # ---------- add a Drill ------------ #
  612. if current_tool not in self.tools:
  613. self.tools[current_tool] = {}
  614. if 'drills' in self.tools[current_tool]:
  615. self.tools[current_tool]['drills'].append(Point((x, y)))
  616. else:
  617. self.tools[current_tool]['drills'] = [Point((x, y))]
  618. # log.debug("{:15} {:8} {:8}".format(eline, x, y))
  619. continue
  620. # ## Coordinates with period: Use literally. # ##
  621. match = self.coordsperiod_re.search(eline)
  622. if match:
  623. matchr = self.repeat_re.search(eline)
  624. if matchr:
  625. repeat = int(matchr.group(1))
  626. if match:
  627. # signal that there are drill operations
  628. self.defaults['excellon_drills'] = True
  629. try:
  630. x = float(match.group(1))
  631. repeating_x = current_x
  632. current_x = x
  633. except TypeError:
  634. x = current_x
  635. repeating_x = 0
  636. try:
  637. y = float(match.group(2))
  638. repeating_y = current_y
  639. current_y = y
  640. except TypeError:
  641. y = current_y
  642. repeating_y = 0
  643. if x is None or y is None:
  644. log.error("Missing coordinates")
  645. continue
  646. # ## Excellon Routing parse
  647. if len(re.findall("G00", eline)) > 0:
  648. self.match_routing_start = 'G00'
  649. # signal that there are milling slots operations
  650. self.defaults['excellon_drills'] = False
  651. self.routing_flag = 0
  652. slot_start_x = x
  653. slot_start_y = y
  654. continue
  655. if self.routing_flag == 0:
  656. if len(re.findall("G01", eline)) > 0:
  657. self.match_routing_stop = 'G01'
  658. # signal that there are milling slots operations
  659. self.defaults['excellon_drills'] = False
  660. self.routing_flag = 1
  661. slot_stop_x = x
  662. slot_stop_y = y
  663. # ---------- add a Slot ------------ #
  664. slot = (
  665. Point(slot_start_x, slot_start_y),
  666. Point(slot_stop_x, slot_stop_y)
  667. )
  668. if current_tool not in self.tools:
  669. self.tools[current_tool] = {}
  670. if 'slots' in self.tools[current_tool]:
  671. self.tools[current_tool]['slots'].append(slot)
  672. else:
  673. self.tools[current_tool]['slots'] = [slot]
  674. continue
  675. if self.match_routing_start is None and self.match_routing_stop is None:
  676. # signal that there are drill operations
  677. if repeat == 0:
  678. # signal that there are drill operations
  679. self.defaults['excellon_drills'] = True
  680. # ---------- add a Drill ------------ #
  681. if current_tool not in self.tools:
  682. self.tools[current_tool] = {}
  683. if 'drills' in self.tools[current_tool]:
  684. self.tools[current_tool]['drills'].append(Point((x, y)))
  685. else:
  686. self.tools[current_tool]['drills'] = [Point((x, y))]
  687. else:
  688. coordx = x
  689. coordy = y
  690. while repeat > 0:
  691. if repeating_x:
  692. coordx = (repeat * x) + repeating_x
  693. if repeating_y:
  694. coordy = (repeat * y) + repeating_y
  695. # ---------- add a Drill ------------ #
  696. if current_tool not in self.tools:
  697. self.tools[current_tool] = {}
  698. if 'drills' in self.tools[current_tool]:
  699. self.tools[current_tool]['drills'].append(Point((coordx, coordy)))
  700. else:
  701. self.tools[current_tool]['drills'] = [Point((coordx, coordy))]
  702. repeat -= 1
  703. repeating_x = repeating_y = 0
  704. # log.debug("{:15} {:8} {:8}".format(eline, x, y))
  705. continue
  706. # ### Header ####
  707. if in_header:
  708. # ## Tool definitions # ##
  709. match = self.toolset_re.search(eline)
  710. if match:
  711. # ---------- add a TOOL ------------ #
  712. name = int(match.group(1))
  713. spec = {"C": float(match.group(2)), 'solid_geometry': []}
  714. if name not in self.tools:
  715. self.tools[name] = {}
  716. self.tools[name]['tooldia'] = float(match.group(2))
  717. self.tools[name]['solid_geometry'] = []
  718. log.debug("Tool definition: %s %s" % (name, spec))
  719. continue
  720. # ## Units and number format # ##
  721. match = self.units_re.match(eline)
  722. if match:
  723. self.units = {"METRIC": "MM", "INCH": "IN"}[match.group(1)]
  724. self.excellon_units_found = self.units
  725. self.zeros = match.group(2) # "T" or "L". Might be empty
  726. self.excellon_format = match.group(3)
  727. if self.excellon_format:
  728. upper = len(self.excellon_format.partition('.')[0])
  729. lower = len(self.excellon_format.partition('.')[2])
  730. if self.units == 'MM':
  731. self.excellon_format_upper_mm = upper
  732. self.excellon_format_lower_mm = lower
  733. else:
  734. self.excellon_format_upper_in = upper
  735. self.excellon_format_lower_in = lower
  736. # Modified for issue #80
  737. log.warning("UNITS found inline - Value before conversion: %s" % self.units)
  738. self.convert_units(self.units)
  739. log.warning("UNITS found inline - Value after conversion: %s" % self.units)
  740. if self.units == 'MM':
  741. log.warning("Excellon format preset is: %s:%s" %
  742. (str(self.excellon_format_upper_mm), str(self.excellon_format_lower_mm)))
  743. else:
  744. log.warning("Excellon format preset is: %s:%s" %
  745. (str(self.excellon_format_upper_in), str(self.excellon_format_lower_in)))
  746. log.warning("Type of ZEROS found inline, in header: %s" % self.zeros)
  747. continue
  748. # Search for units type again it might be alone on the line
  749. if "INCH" in eline:
  750. line_units = "IN"
  751. # Modified for issue #80
  752. log.warning("Type of UNITS found inline, in header, before conversion: %s" % line_units)
  753. self.convert_units(line_units)
  754. log.warning("Type of UNITS found inline, in header, after conversion: %s" % self.units)
  755. log.warning("Excellon format preset is: %s:%s" %
  756. (str(self.excellon_format_upper_in), str(self.excellon_format_lower_in)))
  757. self.excellon_units_found = "IN"
  758. continue
  759. elif "METRIC" in eline:
  760. line_units = "MM"
  761. # Modified for issue #80
  762. log.warning("Type of UNITS found inline, in header, before conversion: %s" % line_units)
  763. self.convert_units(line_units)
  764. log.warning("Type of UNITS found inline, in header, after conversion: %s" % self.units)
  765. log.warning("Excellon format preset is: %s:%s" %
  766. (str(self.excellon_format_upper_mm), str(self.excellon_format_lower_mm)))
  767. self.excellon_units_found = "MM"
  768. continue
  769. # Search for zeros type again because it might be alone on the line
  770. match = re.search(r'[LT]Z', eline)
  771. if match:
  772. self.zeros = match.group()
  773. log.warning("Type of ZEROS found: %s" % self.zeros)
  774. continue
  775. # ## Units and number format outside header# ##
  776. match = self.units_re.match(eline)
  777. if match:
  778. self.units = {"METRIC": "MM", "INCH": "IN"}[match.group(1)]
  779. self.excellon_units_found = self.units
  780. self.zeros = match.group(2) # "T" or "L". Might be empty
  781. self.excellon_format = match.group(3)
  782. if self.excellon_format:
  783. upper = len(self.excellon_format.partition('.')[0])
  784. lower = len(self.excellon_format.partition('.')[2])
  785. if self.units == 'MM':
  786. self.excellon_format_upper_mm = upper
  787. self.excellon_format_lower_mm = lower
  788. else:
  789. self.excellon_format_upper_in = upper
  790. self.excellon_format_lower_in = lower
  791. # Modified for issue #80
  792. log.warning("Type of UNITS found outside header, inline before conversion: %s" % self.units)
  793. self.convert_units(self.units)
  794. log.warning("Type of UNITS found outside header, inline after conversion: %s" % self.units)
  795. if self.units == 'MM':
  796. log.warning("Excellon format preset is: %s:%s" %
  797. (str(self.excellon_format_upper_mm), str(self.excellon_format_lower_mm)))
  798. else:
  799. log.warning("Excellon format preset is: %s:%s" %
  800. (str(self.excellon_format_upper_in), str(self.excellon_format_lower_in)))
  801. log.warning("Type of ZEROS found outside header, inline: %s" % self.zeros)
  802. continue
  803. log.warning("Line ignored: %s" % eline)
  804. # make sure that since we are in headerless mode, we convert the tools only after the file parsing
  805. # is finished since the tools definitions are spread in the Excellon body. We use as units the value
  806. # from self.defaults['excellon_units']
  807. # the data structure of the Excellon object has to include bot the 'drills' and the 'slots' keys otherwise
  808. # I will need to test for them everywhere.
  809. # Even if there are not drills or slots I just add the storage there with an empty list
  810. for tool in self.tools:
  811. if 'drills' not in self.tools[tool]:
  812. self.tools[tool]['drills'] = []
  813. if 'slots' not in self.tools[tool]:
  814. self.tools[tool]['slots'] = []
  815. log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
  816. except Exception:
  817. log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
  818. msg = '[ERROR_NOTCL] %s' % _("An internal error has occurred. See shell.\n")
  819. msg += '{e_code} {tx} {l_nr}: {line}\n'.format(
  820. e_code='[ERROR]',
  821. tx=_("Excellon Parser error.\nParsing Failed. Line"),
  822. l_nr=line_num,
  823. line=eline)
  824. msg += traceback.format_exc()
  825. self.app.inform.emit(msg)
  826. return "fail"
  827. def parse_number(self, number_str):
  828. """
  829. Parses coordinate numbers without period.
  830. :param number_str: String representing the numerical value.
  831. :type number_str: str
  832. :return: Floating point representation of the number
  833. :rtype: float
  834. """
  835. match = self.leadingzeros_re.search(number_str)
  836. nr_length = len(match.group(1)) + len(match.group(2))
  837. try:
  838. if self.zeros == "L" or self.zeros == "LZ": # Leading
  839. # With leading zeros, when you type in a coordinate,
  840. # the leading zeros must always be included. Trailing zeros
  841. # are unneeded and may be left off. The CNC-7 will automatically add them.
  842. # r'^[-\+]?(0*)(\d*)'
  843. # 6 digits are divided by 10^4
  844. # If less than size digits, they are automatically added,
  845. # 5 digits then are divided by 10^3 and so on.
  846. if self.units.lower() == "in":
  847. result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_in)))
  848. else:
  849. result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_mm)))
  850. return result
  851. else: # Trailing
  852. # You must show all zeros to the right of the number and can omit
  853. # all zeros to the left of the number. The CNC-7 will count the number
  854. # of digits you typed and automatically fill in the missing zeros.
  855. # ## flatCAM expects 6digits
  856. # flatCAM expects the number of digits entered into the defaults
  857. if self.units.lower() == "in": # Inches is 00.0000
  858. result = float(number_str) / (10 ** (float(self.excellon_format_lower_in)))
  859. else: # Metric is 000.000
  860. result = float(number_str) / (10 ** (float(self.excellon_format_lower_mm)))
  861. return result
  862. except Exception as e:
  863. log.error("Aborted. Operation could not be completed due of %s" % str(e))
  864. return
  865. def create_geometry(self):
  866. """
  867. Creates circles of the tool diameter at every point
  868. specified in self.tools[tool]['drills'].
  869. Also creates geometries (polygons)
  870. for the slots as specified in self.tools[tool]['slots']
  871. All the resulting geometry is stored into self.solid_geometry list.
  872. The list self.solid_geometry has 2 elements: first is a dict with the drills geometry,
  873. and second element is another similar dict that contain the slots geometry.
  874. Each dict has as keys the tool diameters and as values lists with Shapely objects, the geometries
  875. ================ ====================================
  876. Key Value
  877. ================ ====================================
  878. tool_diameter list of (Shapely.Point) Where to drill
  879. ================ ====================================
  880. :return: None
  881. """
  882. log.debug("appParsers.ParseExcellon.Excellon.create_geometry()")
  883. self.solid_geometry = []
  884. try:
  885. # clear the solid_geometry in self.tools
  886. for tool in self.tools:
  887. self.tools[tool]['solid_geometry'] = []
  888. self.tools[tool]['data'] = {}
  889. for tool in self.tools:
  890. tooldia = self.tools[tool]['tooldia']
  891. if 'drills' in self.tools[tool]:
  892. for drill in self.tools[tool]['drills']:
  893. poly = drill.buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
  894. # add poly in the tools geometry
  895. self.tools[tool]['solid_geometry'].append(poly)
  896. self.tools[tool]['data'] = deepcopy(self.default_data)
  897. # add poly to the total solid geometry
  898. self.solid_geometry.append(poly)
  899. if 'slots' in self.tools[tool]:
  900. for slot in self.tools[tool]['slots']:
  901. start = slot[0]
  902. stop = slot[1]
  903. lines_string = LineString([start, stop])
  904. poly = lines_string.buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
  905. # add poly in the tools geometry
  906. self.tools[tool]['solid_geometry'].append(poly)
  907. self.tools[tool]['data'] = deepcopy(self.default_data)
  908. # add poly to the total solid geometry
  909. self.solid_geometry.append(poly)
  910. except Exception as e:
  911. log.debug("appParsers.ParseExcellon.Excellon.create_geometry() -> "
  912. "Excellon geometry creation failed due of ERROR: %s" % str(e))
  913. return "fail"
  914. def bounds(self, flatten=None):
  915. """
  916. Returns coordinates of rectangular bounds
  917. of Excellon geometry: (xmin, ymin, xmax, ymax).
  918. :param flatten: No used
  919. """
  920. log.debug("appParsers.ParseExcellon.Excellon.bounds()")
  921. if self.solid_geometry is None or not self.tools:
  922. log.debug("appParsers.ParseExcellon.Excellon -> solid_geometry is None")
  923. return 0, 0, 0, 0
  924. def bounds_rec(obj):
  925. if type(obj) is list:
  926. minx = np.Inf
  927. miny = np.Inf
  928. maxx = -np.Inf
  929. maxy = -np.Inf
  930. for k in obj:
  931. if type(k) is dict:
  932. for key in k:
  933. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  934. minx = min(minx, minx_)
  935. miny = min(miny, miny_)
  936. maxx = max(maxx, maxx_)
  937. maxy = max(maxy, maxy_)
  938. else:
  939. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  940. minx = min(minx, minx_)
  941. miny = min(miny, miny_)
  942. maxx = max(maxx, maxx_)
  943. maxy = max(maxy, maxy_)
  944. return minx, miny, maxx, maxy
  945. else:
  946. # it's a Shapely object, return it's bounds
  947. return obj.bounds
  948. minx_list = []
  949. miny_list = []
  950. maxx_list = []
  951. maxy_list = []
  952. for tool in self.tools:
  953. eminx, eminy, emaxx, emaxy = bounds_rec(self.tools[tool]['solid_geometry'])
  954. minx_list.append(eminx)
  955. miny_list.append(eminy)
  956. maxx_list.append(emaxx)
  957. maxy_list.append(emaxy)
  958. return min(minx_list), min(miny_list), max(maxx_list), max(maxy_list)
  959. def convert_units(self, units):
  960. """
  961. This function first convert to the the units found in the Excellon file but it converts tools that
  962. are not there yet so it has no effect other than it signal that the units are the ones in the file.
  963. On object creation, in app_obj.new_object(), true conversion is done because this is done at the end of the
  964. Excellon file parsing, the tools are inside and self.tools is really converted from the units found
  965. inside the file to the FlatCAM units.
  966. Kind of convolute way to make the conversion and it is based on the assumption that the Excellon file
  967. will have detected the units before the tools are parsed and stored in self.tools
  968. :param units: 'IN' or 'MM'. String
  969. :return:
  970. """
  971. # factor = Geometry.convert_units(self, units)
  972. obj_units = units
  973. if obj_units.upper() == self.units.upper():
  974. factor = 1.0
  975. elif obj_units.upper() == "MM":
  976. factor = 25.4
  977. elif obj_units.upper() == "IN":
  978. factor = 1 / 25.4
  979. else:
  980. log.error("Unsupported units: %s" % str(obj_units))
  981. factor = 1.0
  982. log.debug("appParsers.ParseExcellon.Excellon.convert_units() --> Factor: %s" % str(factor))
  983. self.units = obj_units
  984. self.scale(factor, factor)
  985. self.file_units_factor = factor
  986. # Tools
  987. for tname in self.tools:
  988. self.tools[tname]["tooldia"] *= factor
  989. self.create_geometry()
  990. return factor
  991. def scale(self, xfactor, yfactor=None, point=None):
  992. """
  993. Scales geometry on the XY plane in the object by a given factor.
  994. Tool sizes, feedrates an Z-plane dimensions are untouched.
  995. :param xfactor: Number by which to scale the object.
  996. :type xfactor: float
  997. :param yfactor: Number by which to scale the object.
  998. :type yfactor: float
  999. :param point: Origin point for scale
  1000. :return: None
  1001. :rtype: None
  1002. """
  1003. log.debug("appParsers.ParseExcellon.Excellon.scale()")
  1004. if yfactor is None:
  1005. yfactor = xfactor
  1006. if point is None:
  1007. px = 0
  1008. py = 0
  1009. else:
  1010. px, py = point
  1011. if xfactor == 0 and yfactor == 0:
  1012. return
  1013. def scale_geom(obj):
  1014. if type(obj) is list:
  1015. new_obj = []
  1016. for g in obj:
  1017. new_obj.append(scale_geom(g))
  1018. return new_obj
  1019. else:
  1020. try:
  1021. return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
  1022. except AttributeError:
  1023. return obj
  1024. # variables to display the percentage of work done
  1025. self.geo_len = 0
  1026. try:
  1027. self.geo_len = len(self.tools)
  1028. except TypeError:
  1029. self.geo_len = 1
  1030. self.old_disp_number = 0
  1031. self.el_count = 0
  1032. for tool in self.tools:
  1033. # Scale Drills
  1034. if 'drills' in self.tools[tool]:
  1035. new_drills = []
  1036. for drill in self.tools[tool]['drills']:
  1037. new_drills.append(affinity.scale(drill, xfactor, yfactor, origin=(px, py)))
  1038. self.tools[tool]['drills'] = new_drills
  1039. # Scale Slots
  1040. if 'slots' in self.tools[tool]:
  1041. new_slots = []
  1042. for slot in self.tools[tool]['slots']:
  1043. new_start = affinity.scale(slot[0], xfactor, yfactor, origin=(px, py))
  1044. new_stop = affinity.scale(slot[1], xfactor, yfactor, origin=(px, py))
  1045. new_slot = (new_start, new_stop)
  1046. new_slots.append(new_slot)
  1047. self.tools[tool]['slots'] = new_slots
  1048. # Scale solid_geometry
  1049. self.tools[tool]['solid_geometry'] = scale_geom(self.tools[tool]['solid_geometry'])
  1050. # update status display
  1051. self.el_count += 1
  1052. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  1053. if self.old_disp_number < disp_number <= 100:
  1054. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1055. self.old_disp_number = disp_number
  1056. self.create_geometry()
  1057. self.app.proc_container.new_text = ''
  1058. def offset(self, vect):
  1059. """
  1060. Offsets geometry on the XY plane in the object by a given vector.
  1061. :param vect: (x, y) offset vector.
  1062. :type vect: tuple
  1063. :return: None
  1064. """
  1065. log.debug("appParsers.ParseExcellon.Excellon.offset()")
  1066. dx, dy = vect
  1067. if dx == 0 and dy == 0:
  1068. return
  1069. def offset_geom(obj):
  1070. try:
  1071. new_obj = []
  1072. for geo in obj:
  1073. new_obj.append(offset_geom(geo))
  1074. return new_obj
  1075. except TypeError:
  1076. try:
  1077. return affinity.translate(obj, xoff=dx, yoff=dy)
  1078. except AttributeError:
  1079. return obj
  1080. # variables to display the percentage of work done
  1081. self.geo_len = 0
  1082. try:
  1083. self.geo_len = len(self.tools)
  1084. except TypeError:
  1085. self.geo_len = 1
  1086. self.old_disp_number = 0
  1087. self.el_count = 0
  1088. for tool in self.tools:
  1089. # Offset Drills
  1090. if 'drills' in self.tools[tool]:
  1091. new_drills = []
  1092. for drill in self.tools[tool]['drills']:
  1093. new_drills.append(affinity.translate(drill, xoff=dx, yoff=dy))
  1094. self.tools[tool]['drills'] = new_drills
  1095. # Offset Slots
  1096. if 'slots' in self.tools[tool]:
  1097. new_slots = []
  1098. for slot in self.tools[tool]['slots']:
  1099. new_start = affinity.translate(slot[0], xoff=dx, yoff=dy)
  1100. new_stop = affinity.translate(slot[1], xoff=dx, yoff=dy)
  1101. new_slot = (new_start, new_stop)
  1102. new_slots.append(new_slot)
  1103. self.tools[tool]['slots'] = new_slots
  1104. # Offset solid_geometry
  1105. self.tools[tool]['solid_geometry'] = offset_geom(self.tools[tool]['solid_geometry'])
  1106. # update status display
  1107. self.el_count += 1
  1108. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  1109. if self.old_disp_number < disp_number <= 100:
  1110. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1111. self.old_disp_number = disp_number
  1112. # Recreate geometry
  1113. self.create_geometry()
  1114. self.app.proc_container.new_text = ''
  1115. def mirror(self, axis, point):
  1116. """
  1117. :param axis: "X" or "Y" indicates around which axis to mirror.
  1118. :type axis: str
  1119. :param point: [x, y] point belonging to the mirror axis.
  1120. :type point: list
  1121. :return: None
  1122. """
  1123. log.debug("appParsers.ParseExcellon.Excellon.mirror()")
  1124. px, py = point
  1125. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  1126. def mirror_geom(obj):
  1127. try:
  1128. new_obj = []
  1129. for geo in obj:
  1130. new_obj.append(mirror_geom(geo))
  1131. return new_obj
  1132. except TypeError:
  1133. try:
  1134. return affinity.scale(obj, xscale, yscale, origin=(px, py))
  1135. except AttributeError:
  1136. return obj
  1137. # Modify data
  1138. # variables to display the percentage of work done
  1139. self.geo_len = 0
  1140. try:
  1141. self.geo_len = len(self.tools)
  1142. except TypeError:
  1143. self.geo_len = 1
  1144. self.old_disp_number = 0
  1145. self.el_count = 0
  1146. for tool in self.tools:
  1147. # Offset Drills
  1148. if 'drills' in self.tools[tool]:
  1149. new_drills = []
  1150. for drill in self.tools[tool]['drills']:
  1151. new_drills.append(affinity.scale(drill, xscale, yscale, origin=(px, py)))
  1152. self.tools[tool]['drills'] = new_drills
  1153. # Offset Slots
  1154. if 'slots' in self.tools[tool]:
  1155. new_slots = []
  1156. for slot in self.tools[tool]['slots']:
  1157. new_start = affinity.scale(slot[0], xscale, yscale, origin=(px, py))
  1158. new_stop = affinity.scale(slot[1], xscale, yscale, origin=(px, py))
  1159. new_slot = (new_start, new_stop)
  1160. new_slots.append(new_slot)
  1161. self.tools[tool]['slots'] = new_slots
  1162. # Offset solid_geometry
  1163. self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry'])
  1164. # update status display
  1165. self.el_count += 1
  1166. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  1167. if self.old_disp_number < disp_number <= 100:
  1168. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1169. self.old_disp_number = disp_number
  1170. # Recreate geometry
  1171. self.create_geometry()
  1172. self.app.proc_container.new_text = ''
  1173. def skew(self, angle_x=None, angle_y=None, point=None):
  1174. """
  1175. Shear/Skew the geometries of an object by angles along x and y dimensions.
  1176. Tool sizes, feedrates an Z-plane dimensions are untouched.
  1177. :param angle_x:
  1178. :param angle_y:
  1179. The shear angle(s) for the x and y axes respectively. These can be
  1180. specified in either degrees (default) or radians by setting
  1181. use_radians=True.
  1182. :param point: Origin point for Skew
  1183. See shapely manual for more information:
  1184. http://toblerity.org/shapely/manual.html#affine-transformations
  1185. """
  1186. log.debug("appParsers.ParseExcellon.Excellon.skew()")
  1187. if angle_x is None:
  1188. angle_x = 0.0
  1189. if angle_y is None:
  1190. angle_y = 0.0
  1191. if angle_x == 0 and angle_y == 0:
  1192. return
  1193. def skew_geom(obj):
  1194. try:
  1195. new_obj = []
  1196. for g in obj:
  1197. new_obj.append(skew_geom(g))
  1198. return new_obj
  1199. except TypeError:
  1200. try:
  1201. return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
  1202. except AttributeError:
  1203. return obj
  1204. # variables to display the percentage of work done
  1205. self.geo_len = 0
  1206. try:
  1207. self.geo_len = len(self.tools)
  1208. except TypeError:
  1209. self.geo_len = 1
  1210. self.old_disp_number = 0
  1211. self.el_count = 0
  1212. if point is None:
  1213. px, py = 0, 0
  1214. else:
  1215. px, py = point
  1216. for tool in self.tools:
  1217. # Offset Drills
  1218. if 'drills' in self.tools[tool]:
  1219. new_drills = []
  1220. for drill in self.tools[tool]['drills']:
  1221. new_drills.append(affinity.skew(drill, angle_x, angle_y, origin=(px, py)))
  1222. self.tools[tool]['drills'] = new_drills
  1223. # Offset Slots
  1224. if 'slots' in self.tools[tool]:
  1225. new_slots = []
  1226. for slot in self.tools[tool]['slots']:
  1227. new_start = affinity.skew(slot[0], angle_x, angle_y, origin=(px, py))
  1228. new_stop = affinity.skew(slot[1], angle_x, angle_y, origin=(px, py))
  1229. new_slot = (new_start, new_stop)
  1230. new_slots.append(new_slot)
  1231. self.tools[tool]['slots'] = new_slots
  1232. # Offset solid_geometry
  1233. self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
  1234. # update status display
  1235. self.el_count += 1
  1236. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  1237. if self.old_disp_number < disp_number <= 100:
  1238. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1239. self.old_disp_number = disp_number
  1240. self.create_geometry()
  1241. self.app.proc_container.new_text = ''
  1242. def rotate(self, angle, point=None):
  1243. """
  1244. Rotate the geometry of an object by an angle around the 'point' coordinates
  1245. :param angle:
  1246. :param point: tuple of coordinates (x, y)
  1247. :return: None
  1248. """
  1249. log.debug("appParsers.ParseExcellon.Excellon.rotate()")
  1250. if angle == 0:
  1251. return
  1252. def rotate_geom(obj, origin=None):
  1253. if type(obj) is list:
  1254. new_obj = []
  1255. for g in obj:
  1256. new_obj.append(rotate_geom(g))
  1257. return new_obj
  1258. else:
  1259. if origin:
  1260. try:
  1261. return affinity.rotate(obj, angle, origin=origin)
  1262. except AttributeError:
  1263. return obj
  1264. else:
  1265. try:
  1266. return affinity.rotate(obj, angle, origin=orig)
  1267. except AttributeError:
  1268. return obj
  1269. # variables to display the percentage of work done
  1270. self.geo_len = 0
  1271. try:
  1272. self.geo_len = len(self.tools)
  1273. except TypeError:
  1274. self.geo_len = 1
  1275. self.old_disp_number = 0
  1276. self.el_count = 0
  1277. if point is None:
  1278. orig = 'center'
  1279. else:
  1280. orig = point
  1281. for tool in self.tools:
  1282. # Offset Drills
  1283. if 'drills' in self.tools[tool]:
  1284. new_drills = []
  1285. for drill in self.tools[tool]['drills']:
  1286. new_drills.append(affinity.rotate(drill, angle, origin=orig))
  1287. self.tools[tool]['drills'] = new_drills
  1288. # Offset Slots
  1289. if 'slots' in self.tools[tool]:
  1290. new_slots = []
  1291. for slot in self.tools[tool]['slots']:
  1292. new_start = affinity.rotate(slot[0], angle, origin=orig)
  1293. new_stop = affinity.rotate(slot[1], angle, origin=orig)
  1294. new_slot = (new_start, new_stop)
  1295. new_slots.append(new_slot)
  1296. self.tools[tool]['slots'] = new_slots
  1297. # Offset solid_geometry
  1298. self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'], origin=orig)
  1299. # update status display
  1300. self.el_count += 1
  1301. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  1302. if self.old_disp_number < disp_number <= 100:
  1303. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1304. self.old_disp_number = disp_number
  1305. self.create_geometry()
  1306. self.app.proc_container.new_text = ''
  1307. def buffer(self, distance, join, factor):
  1308. """
  1309. :param distance: if 'factor' is True then distance is the factor
  1310. :param factor: True or False (None)
  1311. :param join: The type of line joint used by the shapely buffer method: round, square, bevel
  1312. :return: None
  1313. """
  1314. log.debug("appParsers.ParseExcellon.Excellon.buffer()")
  1315. if distance == 0:
  1316. return
  1317. def buffer_geom(obj):
  1318. try:
  1319. new_obj = []
  1320. for g in obj:
  1321. new_obj.append(buffer_geom(g))
  1322. return new_obj
  1323. except TypeError:
  1324. try:
  1325. if factor is None:
  1326. return obj.buffer(distance, resolution=self.geo_steps_per_circle)
  1327. else:
  1328. return affinity.scale(obj, xfact=distance, yfact=distance, origin='center')
  1329. except AttributeError:
  1330. return obj
  1331. # buffer solid_geometry
  1332. for tool, tool_dict in list(self.tools.items()):
  1333. res = buffer_geom(tool_dict['solid_geometry'])
  1334. try:
  1335. __ = iter(res)
  1336. self.tools[tool]['solid_geometry'] = res
  1337. except TypeError:
  1338. self.tools[tool]['solid_geometry'] = [res]
  1339. if factor is None:
  1340. self.tools[tool]['tooldia'] += distance
  1341. else:
  1342. self.tools[tool]['tooldia'] *= distance
  1343. self.create_geometry()