ParseHPGL2.py 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067
  1. # ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # File Author: Marius Adrian Stanciu (c) #
  5. # Date: 12/11/2019 #
  6. # MIT Licence #
  7. # ############################################################
  8. from camlib import Geometry, arc, arc_angle
  9. import FlatCAMApp
  10. import numpy as np
  11. import re
  12. import logging
  13. import traceback
  14. from copy import deepcopy
  15. import sys
  16. from shapely.ops import cascaded_union, unary_union
  17. from shapely.geometry import Polygon, MultiPolygon, LineString, Point, MultiLineString
  18. import shapely.affinity as affinity
  19. from shapely.geometry import box as shply_box
  20. import FlatCAMTranslation as fcTranslate
  21. import gettext
  22. import builtins
  23. if '_' not in builtins.__dict__:
  24. _ = gettext.gettext
  25. log = logging.getLogger('base')
  26. class HPGL2(Geometry):
  27. """
  28. HPGL2 parsing.
  29. """
  30. defaults = {
  31. "steps_per_circle": 64,
  32. "use_buffer_for_union": True
  33. }
  34. def __init__(self, steps_per_circle=None):
  35. """
  36. The constructor takes no parameters.
  37. :return: Geometry object
  38. :rtype: Geometry
  39. """
  40. # How to approximate a circle with lines.
  41. self.steps_per_circle = steps_per_circle if steps_per_circle is not None else \
  42. int(self.app.defaults["geometry_circle_steps"])
  43. self.decimals = self.app.decimals
  44. # Initialize parent
  45. Geometry.__init__(self, geo_steps_per_circle=self.steps_per_circle)
  46. # Number format
  47. self.coord_mm_factor = 0.040
  48. # store the file units here:
  49. self.units = 'MM'
  50. # storage for the tools
  51. self.tools = dict()
  52. self.default_data = dict()
  53. self.default_data.update({
  54. "name": '_ncc',
  55. "plot": self.app.defaults["geometry_plot"],
  56. "cutz": self.app.defaults["geometry_cutz"],
  57. "vtipdia": self.app.defaults["geometry_vtipdia"],
  58. "vtipangle": self.app.defaults["geometry_vtipangle"],
  59. "travelz": self.app.defaults["geometry_travelz"],
  60. "feedrate": self.app.defaults["geometry_feedrate"],
  61. "feedrate_z": self.app.defaults["geometry_feedrate_z"],
  62. "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
  63. "dwell": self.app.defaults["geometry_dwell"],
  64. "dwelltime": self.app.defaults["geometry_dwelltime"],
  65. "multidepth": self.app.defaults["geometry_multidepth"],
  66. "ppname_g": self.app.defaults["geometry_ppname_g"],
  67. "depthperpass": self.app.defaults["geometry_depthperpass"],
  68. "extracut": self.app.defaults["geometry_extracut"],
  69. "extracut_length": self.app.defaults["geometry_extracut_length"],
  70. "toolchange": self.app.defaults["geometry_toolchange"],
  71. "toolchangez": self.app.defaults["geometry_toolchangez"],
  72. "endz": self.app.defaults["geometry_endz"],
  73. "spindlespeed": self.app.defaults["geometry_spindlespeed"],
  74. "toolchangexy": self.app.defaults["geometry_toolchangexy"],
  75. "startz": self.app.defaults["geometry_startz"],
  76. "tooldia": self.app.defaults["tools_painttooldia"],
  77. "paintmargin": self.app.defaults["tools_paintmargin"],
  78. "paintmethod": self.app.defaults["tools_paintmethod"],
  79. "selectmethod": self.app.defaults["tools_selectmethod"],
  80. "pathconnect": self.app.defaults["tools_pathconnect"],
  81. "paintcontour": self.app.defaults["tools_paintcontour"],
  82. "paintoverlap": self.app.defaults["tools_paintoverlap"],
  83. "nccoverlap": self.app.defaults["tools_nccoverlap"],
  84. "nccmargin": self.app.defaults["tools_nccmargin"],
  85. "nccmethod": self.app.defaults["tools_nccmethod"],
  86. "nccconnect": self.app.defaults["tools_nccconnect"],
  87. "ncccontour": self.app.defaults["tools_ncccontour"],
  88. "nccrest": self.app.defaults["tools_nccrest"]
  89. })
  90. # flag to be set True when tool is detected
  91. self.tool_detected = False
  92. # will store the geometry's as solids
  93. self.solid_geometry = None
  94. # will store the geometry's as paths
  95. self.follow_geometry = []
  96. self.source_file = ''
  97. # Attributes to be included in serialization
  98. # Always append to it because it carries contents
  99. # from Geometry.
  100. self.ser_attrs += ['solid_geometry', 'follow_geometry', 'source_file']
  101. # ### Parser patterns ## ##
  102. # comment
  103. self.comment_re = re.compile(r"^CO\s*[\"']([a-zA-Z0-9\s]*)[\"'];?$")
  104. # absolute move to x, y
  105. self.abs_move_re = re.compile(r"^PA\s*(-?\d+\.?\d+?),?\s*(-?\d+\.?\d+?)*;?$")
  106. # relative move to x, y
  107. self.rel_move_re = re.compile(r"^PR\s*(-?\d+\.\d+?),?\s*(-?\d+\.\d+?)*;?$")
  108. # pen position
  109. self.pen_re = re.compile(r"^(P[U|D]);?$")
  110. # Initialize
  111. self.initialize_re = re.compile(r'^(IN);?$')
  112. # select pen
  113. self.sp_re = re.compile(r'SP(\d);?$')
  114. self.fmt_re_alt = re.compile(r'%FS([LTD])?([AI])X(\d)(\d)Y\d\d\*MO(IN|MM)\*%$')
  115. self.fmt_re_orcad = re.compile(r'(G\d+)*\**%FS([LTD])?([AI]).*X(\d)(\d)Y\d\d\*%$')
  116. # G01... - Linear interpolation plus flashes with coordinates
  117. # Operation code (D0x) missing is deprecated... oh well I will support it.
  118. self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X([+-]?\d+))?(?=.*Y([+-]?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$')
  119. # G02/3... - Circular interpolation with coordinates
  120. # 2-clockwise, 3-counterclockwise
  121. # Operation code (D0x) missing is deprecated... oh well I will support it.
  122. # Optional start with G02 or G03, optional end with D01 or D02 with
  123. # optional coordinates but at least one in any order.
  124. self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X([+-]?\d+))?(?=.*Y([+-]?\d+))' +
  125. '?(?=.*I([+-]?\d+))?(?=.*J([+-]?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$')
  126. # Absolute/Relative G90/1 (OBSOLETE)
  127. self.absrel_re = re.compile(r'^G9([01])\*$')
  128. # flag to store if a conversion was done. It is needed because multiple units declarations can be found
  129. # in a Gerber file (normal or obsolete ones)
  130. self.conversion_done = False
  131. self.in_header = None
  132. def parse_file(self, filename):
  133. """
  134. :param filename: HPGL2 file to parse.
  135. :type filename: str
  136. :return: None
  137. """
  138. with open(filename, 'r') as gfile:
  139. self.parse_lines([line.rstrip('\n') for line in gfile])
  140. def parse_lines(self, glines):
  141. """
  142. Main HPGL2 parser.
  143. :param glines: HPGL2 code as list of strings, each element being
  144. one line of the source file.
  145. :type glines: list
  146. :return: None
  147. :rtype: None
  148. """
  149. # Coordinates of the current path, each is [x, y]
  150. path = list()
  151. geo_buffer = []
  152. # Current coordinates
  153. current_x = None
  154. current_y = None
  155. previous_x = None
  156. previous_y = None
  157. # store the pen (tool) status
  158. pen_status = 'up'
  159. # store the current tool here
  160. current_tool = None
  161. # ### Parsing starts here ## ##
  162. line_num = 0
  163. gline = ""
  164. self.app.inform.emit('%s %d %s.' % (_("HPGL2 processing. Parsing"), len(glines), _("lines")))
  165. try:
  166. for gline in glines:
  167. if self.app.abort_flag:
  168. # graceful abort requested by the user
  169. raise FlatCAMApp.GracefulException
  170. line_num += 1
  171. self.source_file += gline + '\n'
  172. # Cleanup #
  173. gline = gline.strip(' \r\n')
  174. # log.debug("Line=%3s %s" % (line_num, gline))
  175. # ###################
  176. # Ignored lines #####
  177. # Comments #####
  178. # ###################
  179. match = self.comment_re.search(gline)
  180. if match:
  181. log.debug(str(match.group(1)))
  182. continue
  183. # #####################################################
  184. # Absolute/relative coordinates G90/1 OBSOLETE ########
  185. # #####################################################
  186. match = self.absrel_re.search(gline)
  187. if match:
  188. absolute = {'0': "Absolute", '1': "Relative"}[match.group(1)]
  189. log.warning("Gerber obsolete coordinates type found = %s (Absolute or Relative) " % absolute)
  190. continue
  191. # search for the initialization
  192. match = self.initialize_re.search(gline)
  193. if match:
  194. self.in_header = False
  195. continue
  196. if self.in_header is False:
  197. # tools detection
  198. match = self.sp_re.search(gline)
  199. if match:
  200. tool = match.group(1)
  201. # self.tools[tool] = dict()
  202. self.tools.update({
  203. tool: {
  204. 'tooldia': float('%.*f' %
  205. (
  206. self.decimals,
  207. float(self.app.defaults['geometry_cnctooldia'])
  208. )
  209. ),
  210. 'offset': 'Path',
  211. 'offset_value': 0.0,
  212. 'type': 'Iso',
  213. 'tool_type': 'C1',
  214. 'data': deepcopy(self.default_data),
  215. 'solid_geometry': list()
  216. }
  217. })
  218. if current_tool:
  219. if path:
  220. geo = LineString(path)
  221. self.tools[current_tool]['solid_geometry'].append(geo)
  222. geo_buffer.append(geo)
  223. path[:] = []
  224. current_tool = tool
  225. continue
  226. # pen status detection
  227. match = self.pen_re.search(gline)
  228. if match:
  229. pen_status = {'PU': 'up', 'PD': 'down'}[match.group(1)]
  230. continue
  231. # linear move
  232. match = self.abs_move_re.search(gline)
  233. if match:
  234. # Parse coordinates
  235. if match.group(1) is not None:
  236. linear_x = parse_number(match.group(1))
  237. current_x = linear_x
  238. else:
  239. linear_x = current_x
  240. if match.group(2) is not None:
  241. linear_y = parse_number(match.group(2))
  242. current_y = linear_y
  243. else:
  244. linear_y = current_y
  245. # Pen down: add segment
  246. if pen_status == 'down':
  247. # if linear_x or linear_y are None, ignore those
  248. if current_x is not None and current_y is not None:
  249. # only add the point if it's a new one otherwise skip it (harder to process)
  250. if path[-1] != [current_x, current_y]:
  251. path.append([current_x, current_y])
  252. else:
  253. self.app.inform.emit('[WARNING] %s: %s' %
  254. (_("Coordinates missing, line ignored"), str(gline)))
  255. elif pen_status == 'up':
  256. if len(path) > 1:
  257. geo = LineString(path)
  258. self.tools[current_tool]['solid_geometry'].append(geo)
  259. geo_buffer.append(geo)
  260. path[:] = []
  261. # if linear_x or linear_y are None, ignore those
  262. if linear_x is not None and linear_y is not None:
  263. path = [[linear_x, linear_y]] # Start new path
  264. else:
  265. self.app.inform.emit('[WARNING] %s: %s' %
  266. (_("Coordinates missing, line ignored"), str(gline)))
  267. # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
  268. continue
  269. # ## Circular interpolation
  270. # -clockwise,
  271. # -counterclockwise
  272. match = self.circ_re.search(gline)
  273. # if match:
  274. # arcdir = [None, None, "cw", "ccw"]
  275. #
  276. # mode, circular_x, circular_y, i, j, d = match.groups()
  277. #
  278. # try:
  279. # circular_x = parse_number(circular_x)
  280. # except Exception as e:
  281. # circular_x = current_x
  282. #
  283. # try:
  284. # circular_y = parse_number(circular_y)
  285. # except Exception as e:
  286. # circular_y = current_y
  287. #
  288. # try:
  289. # i = parse_number(i)
  290. # except Exception as e:
  291. # i = 0
  292. #
  293. # try:
  294. # j = parse_number(j)
  295. # except Exception as e:
  296. # j = 0
  297. #
  298. # if mode is None and current_interpolation_mode not in [2, 3]:
  299. # log.error("Found arc without circular interpolation mode defined. (%d)" % line_num)
  300. # log.error(gline)
  301. # continue
  302. # elif mode is not None:
  303. # current_interpolation_mode = int(mode)
  304. #
  305. # # Set operation code if provided
  306. # if d is not None:
  307. # current_operation_code = int(d)
  308. #
  309. # # Nothing created! Pen Up.
  310. # if current_operation_code == 2:
  311. # log.warning("Arc with D2. (%d)" % line_num)
  312. # if len(path) > 1:
  313. # geo_dict = dict()
  314. #
  315. # if last_path_aperture is None:
  316. # log.warning("No aperture defined for curent path. (%d)" % line_num)
  317. #
  318. # # --- BUFFERED ---
  319. # width = self.apertures[last_path_aperture]["size"]
  320. #
  321. # # this treats the case when we are storing geometry as paths
  322. # geo_f = LineString(path)
  323. # if not geo_f.is_empty:
  324. # geo_dict['follow'] = geo_f
  325. #
  326. # # this treats the case when we are storing geometry as solids
  327. # buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
  328. #
  329. # if last_path_aperture not in self.apertures:
  330. # self.apertures[last_path_aperture] = dict()
  331. # if 'geometry' not in self.apertures[last_path_aperture]:
  332. # self.apertures[last_path_aperture]['geometry'] = []
  333. # self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
  334. #
  335. # current_x = circular_x
  336. # current_y = circular_y
  337. # path = [[current_x, current_y]] # Start new path
  338. # continue
  339. #
  340. # # Flash should not happen here
  341. # if current_operation_code == 3:
  342. # log.error("Trying to flash within arc. (%d)" % line_num)
  343. # continue
  344. #
  345. # if quadrant_mode == 'MULTI':
  346. # center = [i + current_x, j + current_y]
  347. # radius = np.sqrt(i ** 2 + j ** 2)
  348. # start = np.arctan2(-j, -i) # Start angle
  349. # # Numerical errors might prevent start == stop therefore
  350. # # we check ahead of time. This should result in a
  351. # # 360 degree arc.
  352. # if current_x == circular_x and current_y == circular_y:
  353. # stop = start
  354. # else:
  355. # stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
  356. #
  357. # this_arc = arc(center, radius, start, stop,
  358. # arcdir[current_interpolation_mode],
  359. # self.steps_per_circle)
  360. #
  361. # # The last point in the computed arc can have
  362. # # numerical errors. The exact final point is the
  363. # # specified (x, y). Replace.
  364. # this_arc[-1] = (circular_x, circular_y)
  365. #
  366. # # Last point in path is current point
  367. # # current_x = this_arc[-1][0]
  368. # # current_y = this_arc[-1][1]
  369. # current_x, current_y = circular_x, circular_y
  370. #
  371. # # Append
  372. # path += this_arc
  373. # last_path_aperture = current_aperture
  374. #
  375. # continue
  376. #
  377. # if quadrant_mode == 'SINGLE':
  378. #
  379. # center_candidates = [
  380. # [i + current_x, j + current_y],
  381. # [-i + current_x, j + current_y],
  382. # [i + current_x, -j + current_y],
  383. # [-i + current_x, -j + current_y]
  384. # ]
  385. #
  386. # valid = False
  387. # log.debug("I: %f J: %f" % (i, j))
  388. # for center in center_candidates:
  389. # radius = np.sqrt(i ** 2 + j ** 2)
  390. #
  391. # # Make sure radius to start is the same as radius to end.
  392. # radius2 = np.sqrt((center[0] - circular_x) ** 2 + (center[1] - circular_y) ** 2)
  393. # if radius2 < radius * 0.95 or radius2 > radius * 1.05:
  394. # continue # Not a valid center.
  395. #
  396. # # Correct i and j and continue as with multi-quadrant.
  397. # i = center[0] - current_x
  398. # j = center[1] - current_y
  399. #
  400. # start = np.arctan2(-j, -i) # Start angle
  401. # stop = np.arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
  402. # angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode]))
  403. # log.debug("ARC START: %f, %f CENTER: %f, %f STOP: %f, %f" %
  404. # (current_x, current_y, center[0], center[1], circular_x, circular_y))
  405. # log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" %
  406. # (start * 180 / np.pi, stop * 180 / np.pi, arcdir[current_interpolation_mode],
  407. # angle * 180 / np.pi, np.pi / 2 * 180 / np.pi, angle <= (np.pi + 1e-6) / 2))
  408. #
  409. # if angle <= (np.pi + 1e-6) / 2:
  410. # log.debug("########## ACCEPTING ARC ############")
  411. # this_arc = arc(center, radius, start, stop,
  412. # arcdir[current_interpolation_mode],
  413. # self.steps_per_circle)
  414. #
  415. # # Replace with exact values
  416. # this_arc[-1] = (circular_x, circular_y)
  417. #
  418. # # current_x = this_arc[-1][0]
  419. # # current_y = this_arc[-1][1]
  420. # current_x, current_y = circular_x, circular_y
  421. #
  422. # path += this_arc
  423. # last_path_aperture = current_aperture
  424. # valid = True
  425. # break
  426. #
  427. # if valid:
  428. # continue
  429. # else:
  430. # log.warning("Invalid arc in line %d." % line_num)
  431. # ## Line did not match any pattern. Warn user.
  432. log.warning("Line ignored (%d): %s" % (line_num, gline))
  433. if len(geo_buffer) == 0 and len(self.solid_geometry) == 0:
  434. log.error("Object is not HPGL2 file or empty. Aborting Object creation.")
  435. return 'fail'
  436. log.warning("Joining %d polygons." % len(geo_buffer))
  437. self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(geo_buffer)))
  438. new_poly = unary_union(geo_buffer)
  439. self.solid_geometry = new_poly
  440. except Exception as err:
  441. ex_type, ex, tb = sys.exc_info()
  442. traceback.print_tb(tb)
  443. # print traceback.format_exc()
  444. log.error("HPGL2 PARSING FAILED. Line %d: %s" % (line_num, gline))
  445. loc = '%s #%d %s: %s\n' % (_("HPGL2 Line"), line_num, _("HPGL2 Line Content"), gline) + repr(err)
  446. self.app.inform.emit('[ERROR] %s\n%s:' % (_("HPGL2 Parser ERROR"), loc))
  447. def create_geometry(self):
  448. """
  449. :rtype : None
  450. :return: None
  451. """
  452. pass
  453. def get_bounding_box(self, margin=0.0, rounded=False):
  454. """
  455. Creates and returns a rectangular polygon bounding at a distance of
  456. margin from the object's ``solid_geometry``. If margin > 0, the polygon
  457. can optionally have rounded corners of radius equal to margin.
  458. :param margin: Distance to enlarge the rectangular bounding
  459. box in both positive and negative, x and y axes.
  460. :type margin: float
  461. :param rounded: Wether or not to have rounded corners.
  462. :type rounded: bool
  463. :return: The bounding box.
  464. :rtype: Shapely.Polygon
  465. """
  466. bbox = self.solid_geometry.envelope.buffer(margin)
  467. if not rounded:
  468. bbox = bbox.envelope
  469. return bbox
  470. def bounds(self):
  471. """
  472. Returns coordinates of rectangular bounds
  473. of Gerber geometry: (xmin, ymin, xmax, ymax).
  474. """
  475. # fixed issue of getting bounds only for one level lists of objects
  476. # now it can get bounds for nested lists of objects
  477. log.debug("parseGerber.Gerber.bounds()")
  478. if self.solid_geometry is None:
  479. log.debug("solid_geometry is None")
  480. return 0, 0, 0, 0
  481. def bounds_rec(obj):
  482. if type(obj) is list and type(obj) is not MultiPolygon:
  483. minx = np.Inf
  484. miny = np.Inf
  485. maxx = -np.Inf
  486. maxy = -np.Inf
  487. for k in obj:
  488. if type(k) is dict:
  489. for key in k:
  490. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  491. minx = min(minx, minx_)
  492. miny = min(miny, miny_)
  493. maxx = max(maxx, maxx_)
  494. maxy = max(maxy, maxy_)
  495. else:
  496. if not k.is_empty:
  497. try:
  498. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  499. except Exception as e:
  500. log.debug("camlib.Gerber.bounds() --> %s" % str(e))
  501. return
  502. minx = min(minx, minx_)
  503. miny = min(miny, miny_)
  504. maxx = max(maxx, maxx_)
  505. maxy = max(maxy, maxy_)
  506. return minx, miny, maxx, maxy
  507. else:
  508. # it's a Shapely object, return it's bounds
  509. return obj.bounds
  510. bounds_coords = bounds_rec(self.solid_geometry)
  511. return bounds_coords
  512. def convert_units(self, obj_units):
  513. """
  514. Converts the units of the object to ``units`` by scaling all
  515. the geometry appropriately. This call ``scale()``. Don't call
  516. it again in descendants.
  517. :param obj_units: "IN" or "MM"
  518. :type obj_units: str
  519. :return: Scaling factor resulting from unit change.
  520. :rtype: float
  521. """
  522. if obj_units.upper() == self.units.upper():
  523. log.debug("parseGerber.Gerber.convert_units() --> Factor: 1")
  524. return 1.0
  525. if obj_units.upper() == "MM":
  526. factor = 25.4
  527. log.debug("parseGerber.Gerber.convert_units() --> Factor: 25.4")
  528. elif obj_units.upper() == "IN":
  529. factor = 1 / 25.4
  530. log.debug("parseGerber.Gerber.convert_units() --> Factor: %s" % str(1 / 25.4))
  531. else:
  532. log.error("Unsupported units: %s" % str(obj_units))
  533. log.debug("parseGerber.Gerber.convert_units() --> Factor: 1")
  534. return 1.0
  535. self.units = obj_units
  536. self.file_units_factor = factor
  537. self.scale(factor, factor)
  538. return factor
  539. def scale(self, xfactor, yfactor=None, point=None):
  540. """
  541. Scales the objects' geometry on the XY plane by a given factor.
  542. These are:
  543. * ``buffered_paths``
  544. * ``flash_geometry``
  545. * ``solid_geometry``
  546. * ``regions``
  547. NOTE:
  548. Does not modify the data used to create these elements. If these
  549. are recreated, the scaling will be lost. This behavior was modified
  550. because of the complexity reached in this class.
  551. :param xfactor: Number by which to scale on X axis.
  552. :type xfactor: float
  553. :param yfactor: Number by which to scale on Y axis.
  554. :type yfactor: float
  555. :param point: reference point for scaling operation
  556. :rtype : None
  557. """
  558. log.debug("parseGerber.Gerber.scale()")
  559. try:
  560. xfactor = float(xfactor)
  561. except Exception:
  562. self.app.inform.emit('[ERROR_NOTCL] %s' %
  563. _("Scale factor has to be a number: integer or float."))
  564. return
  565. if yfactor is None:
  566. yfactor = xfactor
  567. else:
  568. try:
  569. yfactor = float(yfactor)
  570. except Exception:
  571. self.app.inform.emit('[ERROR_NOTCL] %s' %
  572. _("Scale factor has to be a number: integer or float."))
  573. return
  574. if xfactor == 0 and yfactor == 0:
  575. return
  576. if point is None:
  577. px = 0
  578. py = 0
  579. else:
  580. px, py = point
  581. # variables to display the percentage of work done
  582. self.geo_len = 0
  583. try:
  584. self.geo_len = len(self.solid_geometry)
  585. except TypeError:
  586. self.geo_len = 1
  587. self.old_disp_number = 0
  588. self.el_count = 0
  589. def scale_geom(obj):
  590. if type(obj) is list:
  591. new_obj = []
  592. for g in obj:
  593. new_obj.append(scale_geom(g))
  594. return new_obj
  595. else:
  596. try:
  597. self.el_count += 1
  598. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
  599. if self.old_disp_number < disp_number <= 100:
  600. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  601. self.old_disp_number = disp_number
  602. return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
  603. except AttributeError:
  604. return obj
  605. self.solid_geometry = scale_geom(self.solid_geometry)
  606. self.follow_geometry = scale_geom(self.follow_geometry)
  607. # we need to scale the geometry stored in the Gerber apertures, too
  608. try:
  609. for apid in self.apertures:
  610. new_geometry = list()
  611. if 'geometry' in self.apertures[apid]:
  612. for geo_el in self.apertures[apid]['geometry']:
  613. new_geo_el = dict()
  614. if 'solid' in geo_el:
  615. new_geo_el['solid'] = scale_geom(geo_el['solid'])
  616. if 'follow' in geo_el:
  617. new_geo_el['follow'] = scale_geom(geo_el['follow'])
  618. if 'clear' in geo_el:
  619. new_geo_el['clear'] = scale_geom(geo_el['clear'])
  620. new_geometry.append(new_geo_el)
  621. self.apertures[apid]['geometry'] = deepcopy(new_geometry)
  622. try:
  623. if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O':
  624. self.apertures[apid]['width'] *= xfactor
  625. self.apertures[apid]['height'] *= xfactor
  626. elif str(self.apertures[apid]['type']) == 'P':
  627. self.apertures[apid]['diam'] *= xfactor
  628. self.apertures[apid]['nVertices'] *= xfactor
  629. except KeyError:
  630. pass
  631. try:
  632. if self.apertures[apid]['size'] is not None:
  633. self.apertures[apid]['size'] = float(self.apertures[apid]['size'] * xfactor)
  634. except KeyError:
  635. pass
  636. except Exception as e:
  637. log.debug('camlib.Gerber.scale() Exception --> %s' % str(e))
  638. return 'fail'
  639. self.app.inform.emit('[success] %s' % _("Gerber Scale done."))
  640. self.app.proc_container.new_text = ''
  641. # ## solid_geometry ???
  642. # It's a cascaded union of objects.
  643. # self.solid_geometry = affinity.scale(self.solid_geometry, factor,
  644. # factor, origin=(0, 0))
  645. # # Now buffered_paths, flash_geometry and solid_geometry
  646. # self.create_geometry()
  647. def offset(self, vect):
  648. """
  649. Offsets the objects' geometry on the XY plane by a given vector.
  650. These are:
  651. * ``buffered_paths``
  652. * ``flash_geometry``
  653. * ``solid_geometry``
  654. * ``regions``
  655. NOTE:
  656. Does not modify the data used to create these elements. If these
  657. are recreated, the scaling will be lost. This behavior was modified
  658. because of the complexity reached in this class.
  659. :param vect: (x, y) offset vector.
  660. :type vect: tuple
  661. :return: None
  662. """
  663. log.debug("parseGerber.Gerber.offset()")
  664. try:
  665. dx, dy = vect
  666. except TypeError:
  667. self.app.inform.emit('[ERROR_NOTCL] %s' %
  668. _("An (x,y) pair of values are needed. "
  669. "Probable you entered only one value in the Offset field."))
  670. return
  671. if dx == 0 and dy == 0:
  672. return
  673. # variables to display the percentage of work done
  674. self.geo_len = 0
  675. try:
  676. for __ in self.solid_geometry:
  677. self.geo_len += 1
  678. except TypeError:
  679. self.geo_len = 1
  680. self.old_disp_number = 0
  681. self.el_count = 0
  682. def offset_geom(obj):
  683. if type(obj) is list:
  684. new_obj = []
  685. for g in obj:
  686. new_obj.append(offset_geom(g))
  687. return new_obj
  688. else:
  689. try:
  690. self.el_count += 1
  691. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
  692. if self.old_disp_number < disp_number <= 100:
  693. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  694. self.old_disp_number = disp_number
  695. return affinity.translate(obj, xoff=dx, yoff=dy)
  696. except AttributeError:
  697. return obj
  698. # ## Solid geometry
  699. self.solid_geometry = offset_geom(self.solid_geometry)
  700. self.follow_geometry = offset_geom(self.follow_geometry)
  701. # we need to offset the geometry stored in the Gerber apertures, too
  702. try:
  703. for apid in self.apertures:
  704. if 'geometry' in self.apertures[apid]:
  705. for geo_el in self.apertures[apid]['geometry']:
  706. if 'solid' in geo_el:
  707. geo_el['solid'] = offset_geom(geo_el['solid'])
  708. if 'follow' in geo_el:
  709. geo_el['follow'] = offset_geom(geo_el['follow'])
  710. if 'clear' in geo_el:
  711. geo_el['clear'] = offset_geom(geo_el['clear'])
  712. except Exception as e:
  713. log.debug('camlib.Gerber.offset() Exception --> %s' % str(e))
  714. return 'fail'
  715. self.app.inform.emit('[success] %s' %
  716. _("Gerber Offset done."))
  717. self.app.proc_container.new_text = ''
  718. def mirror(self, axis, point):
  719. """
  720. Mirrors the object around a specified axis passing through
  721. the given point. What is affected:
  722. * ``buffered_paths``
  723. * ``flash_geometry``
  724. * ``solid_geometry``
  725. * ``regions``
  726. NOTE:
  727. Does not modify the data used to create these elements. If these
  728. are recreated, the scaling will be lost. This behavior was modified
  729. because of the complexity reached in this class.
  730. :param axis: "X" or "Y" indicates around which axis to mirror.
  731. :type axis: str
  732. :param point: [x, y] point belonging to the mirror axis.
  733. :type point: list
  734. :return: None
  735. """
  736. log.debug("parseGerber.Gerber.mirror()")
  737. px, py = point
  738. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  739. # variables to display the percentage of work done
  740. self.geo_len = 0
  741. try:
  742. for __ in self.solid_geometry:
  743. self.geo_len += 1
  744. except TypeError:
  745. self.geo_len = 1
  746. self.old_disp_number = 0
  747. self.el_count = 0
  748. def mirror_geom(obj):
  749. if type(obj) is list:
  750. new_obj = []
  751. for g in obj:
  752. new_obj.append(mirror_geom(g))
  753. return new_obj
  754. else:
  755. try:
  756. self.el_count += 1
  757. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
  758. if self.old_disp_number < disp_number <= 100:
  759. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  760. self.old_disp_number = disp_number
  761. return affinity.scale(obj, xscale, yscale, origin=(px, py))
  762. except AttributeError:
  763. return obj
  764. self.solid_geometry = mirror_geom(self.solid_geometry)
  765. self.follow_geometry = mirror_geom(self.follow_geometry)
  766. # we need to mirror the geometry stored in the Gerber apertures, too
  767. try:
  768. for apid in self.apertures:
  769. if 'geometry' in self.apertures[apid]:
  770. for geo_el in self.apertures[apid]['geometry']:
  771. if 'solid' in geo_el:
  772. geo_el['solid'] = mirror_geom(geo_el['solid'])
  773. if 'follow' in geo_el:
  774. geo_el['follow'] = mirror_geom(geo_el['follow'])
  775. if 'clear' in geo_el:
  776. geo_el['clear'] = mirror_geom(geo_el['clear'])
  777. except Exception as e:
  778. log.debug('camlib.Gerber.mirror() Exception --> %s' % str(e))
  779. return 'fail'
  780. self.app.inform.emit('[success] %s' %
  781. _("Gerber Mirror done."))
  782. self.app.proc_container.new_text = ''
  783. def skew(self, angle_x, angle_y, point):
  784. """
  785. Shear/Skew the geometries of an object by angles along x and y dimensions.
  786. Parameters
  787. ----------
  788. angle_x, angle_y : float, float
  789. The shear angle(s) for the x and y axes respectively. These can be
  790. specified in either degrees (default) or radians by setting
  791. use_radians=True.
  792. See shapely manual for more information:
  793. http://toblerity.org/shapely/manual.html#affine-transformations
  794. :param angle_x: the angle on X axis for skewing
  795. :param angle_y: the angle on Y axis for skewing
  796. :param point: reference point for skewing operation
  797. :return None
  798. """
  799. log.debug("parseGerber.Gerber.skew()")
  800. px, py = point
  801. if angle_x == 0 and angle_y == 0:
  802. return
  803. # variables to display the percentage of work done
  804. self.geo_len = 0
  805. try:
  806. self.geo_len = len(self.solid_geometry)
  807. except TypeError:
  808. self.geo_len = 1
  809. self.old_disp_number = 0
  810. self.el_count = 0
  811. def skew_geom(obj):
  812. if type(obj) is list:
  813. new_obj = []
  814. for g in obj:
  815. new_obj.append(skew_geom(g))
  816. return new_obj
  817. else:
  818. try:
  819. self.el_count += 1
  820. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  821. if self.old_disp_number < disp_number <= 100:
  822. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  823. self.old_disp_number = disp_number
  824. return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
  825. except AttributeError:
  826. return obj
  827. self.solid_geometry = skew_geom(self.solid_geometry)
  828. self.follow_geometry = skew_geom(self.follow_geometry)
  829. # we need to skew the geometry stored in the Gerber apertures, too
  830. try:
  831. for apid in self.apertures:
  832. if 'geometry' in self.apertures[apid]:
  833. for geo_el in self.apertures[apid]['geometry']:
  834. if 'solid' in geo_el:
  835. geo_el['solid'] = skew_geom(geo_el['solid'])
  836. if 'follow' in geo_el:
  837. geo_el['follow'] = skew_geom(geo_el['follow'])
  838. if 'clear' in geo_el:
  839. geo_el['clear'] = skew_geom(geo_el['clear'])
  840. except Exception as e:
  841. log.debug('camlib.Gerber.skew() Exception --> %s' % str(e))
  842. return 'fail'
  843. self.app.inform.emit('[success] %s' % _("Gerber Skew done."))
  844. self.app.proc_container.new_text = ''
  845. def rotate(self, angle, point):
  846. """
  847. Rotate an object by a given angle around given coords (point)
  848. :param angle:
  849. :param point:
  850. :return:
  851. """
  852. log.debug("parseGerber.Gerber.rotate()")
  853. px, py = point
  854. if angle == 0:
  855. return
  856. # variables to display the percentage of work done
  857. self.geo_len = 0
  858. try:
  859. for __ in self.solid_geometry:
  860. self.geo_len += 1
  861. except TypeError:
  862. self.geo_len = 1
  863. self.old_disp_number = 0
  864. self.el_count = 0
  865. def rotate_geom(obj):
  866. if type(obj) is list:
  867. new_obj = []
  868. for g in obj:
  869. new_obj.append(rotate_geom(g))
  870. return new_obj
  871. else:
  872. try:
  873. self.el_count += 1
  874. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  875. if self.old_disp_number < disp_number <= 100:
  876. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  877. self.old_disp_number = disp_number
  878. return affinity.rotate(obj, angle, origin=(px, py))
  879. except AttributeError:
  880. return obj
  881. self.solid_geometry = rotate_geom(self.solid_geometry)
  882. self.follow_geometry = rotate_geom(self.follow_geometry)
  883. # we need to rotate the geometry stored in the Gerber apertures, too
  884. try:
  885. for apid in self.apertures:
  886. if 'geometry' in self.apertures[apid]:
  887. for geo_el in self.apertures[apid]['geometry']:
  888. if 'solid' in geo_el:
  889. geo_el['solid'] = rotate_geom(geo_el['solid'])
  890. if 'follow' in geo_el:
  891. geo_el['follow'] = rotate_geom(geo_el['follow'])
  892. if 'clear' in geo_el:
  893. geo_el['clear'] = rotate_geom(geo_el['clear'])
  894. except Exception as e:
  895. log.debug('camlib.Gerber.rotate() Exception --> %s' % str(e))
  896. return 'fail'
  897. self.app.inform.emit('[success] %s' %
  898. _("Gerber Rotate done."))
  899. self.app.proc_container.new_text = ''
  900. def parse_number(strnumber):
  901. """
  902. Parse a single number of HPGL2 coordinates.
  903. :param strnumber: String containing a number
  904. from a coordinate data block, possibly with a leading sign.
  905. :type strnumber: str
  906. :return: The number in floating point.
  907. :rtype: float
  908. """
  909. return float(strnumber) / 40.0 # in milimeters