ParseExcellon.py 62 KB

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