ParseExcellon.py 65 KB

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