ParseExcellon.py 62 KB

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