ParseExcellon.py 70 KB

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