ParseExcellon.py 62 KB

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