ParseExcellon.py 62 KB

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