ParseExcellon.py 65 KB

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