ParseExcellon.py 62 KB

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