ParseExcellon.py 62 KB

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