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