ToolQRCode.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 10/24/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtWidgets, QtCore
  8. from FlatCAMTool import FlatCAMTool
  9. from flatcamGUI.GUIElements import RadioSet, FCTextArea, FCSpinner, FCDoubleSpinner
  10. from flatcamParsers.ParseSVG import *
  11. from shapely.geometry.base import *
  12. from shapely.ops import unary_union
  13. from shapely.affinity import translate
  14. from shapely.geometry import box
  15. from io import StringIO, BytesIO
  16. from collections import Iterable
  17. import logging
  18. from copy import deepcopy
  19. import qrcode
  20. import qrcode.image.svg
  21. from lxml import etree as ET
  22. import gettext
  23. import FlatCAMTranslation as fcTranslate
  24. import builtins
  25. fcTranslate.apply_language('strings')
  26. if '_' not in builtins.__dict__:
  27. _ = gettext.gettext
  28. log = logging.getLogger('base')
  29. class QRCode(FlatCAMTool):
  30. toolName = _("QRCode Tool")
  31. def __init__(self, app):
  32. FlatCAMTool.__init__(self, app)
  33. self.app = app
  34. self.canvas = self.app.plotcanvas
  35. self.decimals = 4
  36. self.units = ''
  37. # ## Title
  38. title_label = QtWidgets.QLabel("%s" % self.toolName)
  39. title_label.setStyleSheet("""
  40. QLabel
  41. {
  42. font-size: 16px;
  43. font-weight: bold;
  44. }
  45. """)
  46. self.layout.addWidget(title_label)
  47. self.layout.addWidget(QtWidgets.QLabel(''))
  48. # ## Grid Layout
  49. i_grid_lay = QtWidgets.QGridLayout()
  50. self.layout.addLayout(i_grid_lay)
  51. i_grid_lay.setColumnStretch(0, 0)
  52. i_grid_lay.setColumnStretch(1, 1)
  53. self.grb_object_combo = QtWidgets.QComboBox()
  54. self.grb_object_combo.setModel(self.app.collection)
  55. self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  56. self.grb_object_combo.setCurrentIndex(1)
  57. self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
  58. self.grbobj_label.setToolTip(
  59. _("Gerber Object to which the QRCode will be added.")
  60. )
  61. i_grid_lay.addWidget(self.grbobj_label, 0, 0)
  62. i_grid_lay.addWidget(self.grb_object_combo, 0, 1, 1, 2)
  63. i_grid_lay.addWidget(QtWidgets.QLabel(''), 1, 0)
  64. # ## Grid Layout
  65. grid_lay = QtWidgets.QGridLayout()
  66. self.layout.addLayout(grid_lay)
  67. grid_lay.setColumnStretch(0, 0)
  68. grid_lay.setColumnStretch(1, 1)
  69. self.qrcode_label = QtWidgets.QLabel('<b>%s</b>' % _('QRCode Parameters'))
  70. self.qrcode_label.setToolTip(
  71. _("Contain the expected calibration points and the\n"
  72. "ones measured.")
  73. )
  74. grid_lay.addWidget(self.qrcode_label, 0, 0, 1, 2)
  75. # VERSION #
  76. self.version_label = QtWidgets.QLabel('%s:' % _("Version"))
  77. self.version_label.setToolTip(
  78. _("QRCode version can have values from 1 (21x21 boxes)\n"
  79. "to 40 (177x177 boxes).")
  80. )
  81. self.version_entry = FCSpinner()
  82. self.version_entry.set_range(1, 40)
  83. self.version_entry.setWrapping(True)
  84. grid_lay.addWidget(self.version_label, 1, 0)
  85. grid_lay.addWidget(self.version_entry, 1, 1)
  86. # ERROR CORRECTION #
  87. self.error_label = QtWidgets.QLabel('%s:' % _("Error correction"))
  88. self.error_label.setToolTip(
  89. _("Parameter that controls the error correction used for the QR Code.\n"
  90. "L = maximum 7% errors can be corrected\n"
  91. "M = maximum 15% errors can be corrected\n"
  92. "Q = maximum 25% errors can be corrected\n"
  93. "H = maximum 30% errors can be corrected.")
  94. )
  95. self.error_radio = RadioSet([{'label': 'L', 'value': 'L'},
  96. {'label': 'M', 'value': 'M'},
  97. {'label': 'Q', 'value': 'Q'},
  98. {'label': 'H', 'value': 'H'}])
  99. self.error_radio.setToolTip(
  100. _("Parameter that controls the error correction used for the QR Code.\n"
  101. "L = maximum 7% errors can be corrected\n"
  102. "M = maximum 15% errors can be corrected\n"
  103. "Q = maximum 25% errors can be corrected\n"
  104. "H = maximum 30% errors can be corrected.")
  105. )
  106. grid_lay.addWidget(self.error_label, 2, 0)
  107. grid_lay.addWidget(self.error_radio, 2, 1)
  108. # BOX SIZE #
  109. self.bsize_label = QtWidgets.QLabel('%s:' % _("Box Size"))
  110. self.bsize_label.setToolTip(
  111. _("Box size control the overall size of the QRcode\n"
  112. "by adjusting the size of each box in the code.")
  113. )
  114. self.bsize_entry = FCSpinner()
  115. self.bsize_entry.set_range(1, 9999)
  116. self.bsize_entry.setWrapping(True)
  117. grid_lay.addWidget(self.bsize_label, 3, 0)
  118. grid_lay.addWidget(self.bsize_entry, 3, 1)
  119. # BORDER SIZE #
  120. self.border_size_label = QtWidgets.QLabel('%s:' % _("Border Size"))
  121. self.border_size_label.setToolTip(
  122. _("Size of the QRCode border. How many boxes thick is the border.\n"
  123. "Default value is 4. The width of the clearance around the QRCode.")
  124. )
  125. self.border_size_entry = FCSpinner()
  126. self.border_size_entry.set_range(1, 9999)
  127. self.border_size_entry.setWrapping(True)
  128. self.border_size_entry.set_value(4)
  129. grid_lay.addWidget(self.border_size_label, 4, 0)
  130. grid_lay.addWidget(self.border_size_entry, 4, 1)
  131. # Text box
  132. self.text_label = QtWidgets.QLabel('%s:' % _("QRCode Data"))
  133. self.text_label.setToolTip(
  134. _("QRCode Data. Alphanumeric text to be encoded in the QRCode.")
  135. )
  136. self.text_data = FCTextArea()
  137. self.text_data.setPlaceholderText(
  138. _("Add here the text to be included in the QRData...")
  139. )
  140. grid_lay.addWidget(self.text_label, 5, 0)
  141. grid_lay.addWidget(self.text_data, 6, 0, 1, 2)
  142. # POLARITY CHOICE #
  143. self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity"))
  144. self.pol_label.setToolTip(
  145. _("Choose the polarity of the QRCode.\n"
  146. "It can be drawn in a negative way (squares are clear)\n"
  147. "or in a positive way (squares are opaque).")
  148. )
  149. self.pol_radio = RadioSet([{'label': _('Negative'), 'value': 'neg'},
  150. {'label': _('Positive'), 'value': 'pos'}])
  151. self.pol_radio.setToolTip(
  152. _("Choose the type of QRCode to be created.\n"
  153. "If added on a Silkscreen Gerber file the QRCode may\n"
  154. "be added as positive. If it is added to a Copper Gerber\n"
  155. "file then perhaps the QRCode can be added as negative.")
  156. )
  157. grid_lay.addWidget(self.pol_label, 7, 0)
  158. grid_lay.addWidget(self.pol_radio, 7, 1)
  159. # BOUNDING BOX TYPE #
  160. self.bb_label = QtWidgets.QLabel('%s:' % _("Bounding Box"))
  161. self.bb_label.setToolTip(
  162. _("The bounding box, meaning the empty space that surrounds\n"
  163. "the QRCode geometry, can have a rounded or a square shape.")
  164. )
  165. self.bb_radio = RadioSet([{'label': _('Rounded'), 'value': 'r'},
  166. {'label': _('Square'), 'value': 's'}])
  167. self.bb_radio.setToolTip(
  168. _("The bounding box, meaning the empty space that surrounds\n"
  169. "the QRCode geometry, can have a rounded or a square shape.")
  170. )
  171. grid_lay.addWidget(self.bb_label, 8, 0)
  172. grid_lay.addWidget(self.bb_radio, 8, 1)
  173. # ## Create QRCode
  174. self.qrcode_button = QtWidgets.QPushButton(_("Create QRCode"))
  175. self.qrcode_button.setToolTip(
  176. _("Create the QRCode object.")
  177. )
  178. grid_lay.addWidget(self.qrcode_button, 9, 0, 1, 2)
  179. grid_lay.addWidget(QtWidgets.QLabel(''), 10, 0)
  180. self.layout.addStretch()
  181. self.grb_object = None
  182. self.box_poly = None
  183. self.proc = None
  184. self.origin = (0, 0)
  185. self.mm = None
  186. self.mr = None
  187. self.kr = None
  188. self.shapes = self.app.move_tool.sel_shapes
  189. self.qrcode_geometry = MultiPolygon()
  190. self.qrcode_utility_geometry = MultiPolygon()
  191. def run(self, toggle=True):
  192. self.app.report_usage("QRCode()")
  193. if toggle:
  194. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  195. if self.app.ui.splitter.sizes()[0] == 0:
  196. self.app.ui.splitter.setSizes([1, 1])
  197. else:
  198. try:
  199. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  200. # if tab is populated with the tool but it does not have the focus, focus on it
  201. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  202. # focus on Tool Tab
  203. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  204. else:
  205. self.app.ui.splitter.setSizes([0, 1])
  206. except AttributeError:
  207. pass
  208. else:
  209. if self.app.ui.splitter.sizes()[0] == 0:
  210. self.app.ui.splitter.setSizes([1, 1])
  211. FlatCAMTool.run(self)
  212. self.set_tool_ui()
  213. self.app.ui.notebook.setTabText(2, _("QRCode Tool"))
  214. def install(self, icon=None, separator=None, **kwargs):
  215. FlatCAMTool.install(self, icon, separator, shortcut='ALT+Q', **kwargs)
  216. def set_tool_ui(self):
  217. self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value()
  218. self.version_entry.set_value(1)
  219. self.error_radio.set_value('M')
  220. self.bsize_entry.set_value(3)
  221. self.border_size_entry.set_value(4)
  222. self.pol_radio.set_value('pos')
  223. self.bb_radio.set_value('r')
  224. # Signals #
  225. self.qrcode_button.clicked.connect(self.execute)
  226. def execute(self):
  227. text_data = self.text_data.get_value()
  228. if text_data == '':
  229. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
  230. return 'fail'
  231. # get the Gerber object on which the QRCode will be inserted
  232. selection_index = self.grb_object_combo.currentIndex()
  233. model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
  234. try:
  235. self.grb_object = model_index.internalPointer().obj
  236. except Exception as e:
  237. log.debug("QRCode.execute() --> %s" % str(e))
  238. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  239. return 'fail'
  240. # we can safely activate the mouse events
  241. self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
  242. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
  243. self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_release)
  244. self.proc = self.app.proc_container.new('%s...' % _("Generating QRCode geometry"))
  245. def job_thread_qr(app_obj):
  246. error_code = {
  247. 'L': qrcode.constants.ERROR_CORRECT_L,
  248. 'M': qrcode.constants.ERROR_CORRECT_M,
  249. 'Q': qrcode.constants.ERROR_CORRECT_Q,
  250. 'H': qrcode.constants.ERROR_CORRECT_H
  251. }[self.error_radio.get_value()]
  252. qr = qrcode.QRCode(
  253. version=self.version_entry.get_value(),
  254. error_correction=error_code,
  255. box_size=self.bsize_entry.get_value(),
  256. border=self.border_size_entry.get_value(),
  257. image_factory=qrcode.image.svg.SvgFragmentImage
  258. )
  259. qr.add_data(text_data)
  260. qr.make()
  261. svg_file = BytesIO()
  262. img = qr.make_image()
  263. img.save(svg_file)
  264. svg_text = StringIO(svg_file.getvalue().decode('UTF-8'))
  265. svg_geometry = self.convert_svg_to_geo(svg_text, units=self.units)
  266. self.qrcode_geometry = deepcopy(svg_geometry)
  267. svg_geometry = unary_union(svg_geometry).buffer(0.0000001).buffer(-0.0000001)
  268. self.qrcode_utility_geometry = svg_geometry
  269. # make a bounding box of the QRCode geometry to help drawing the utility geometry in case it is too
  270. # complicated
  271. try:
  272. a, b, c, d = self.qrcode_utility_geometry.bounds
  273. self.box_poly = box(minx=a, miny=b, maxx=c, maxy=d)
  274. except Exception as e:
  275. log.debug("QRCode.make() bounds error --> %s" % str(e))
  276. app_obj.call_source = 'qrcode_tool'
  277. app_obj.inform.emit(_("Click on the Destination point ..."))
  278. self.app.worker_task.emit({'fcn': job_thread_qr, 'params': [self.app]})
  279. def make(self, pos):
  280. self.on_exit()
  281. # add the svg geometry to the selected Gerber object solid_geometry and in obj.apertures, apid = 0
  282. if not isinstance(self.grb_object.solid_geometry, Iterable):
  283. self.grb_object.solid_geometry = list(self.grb_object.solid_geometry)
  284. # I use the utility geometry (self.qrcode_utility_geometry) because it is already buffered
  285. geo_list = self.grb_object.solid_geometry
  286. if isinstance(self.grb_object.solid_geometry, MultiPolygon):
  287. geo_list = list(self.grb_object.solid_geometry.geoms)
  288. # this is the bounding box of the QRCode geometry
  289. a, b, c, d = self.qrcode_utility_geometry.bounds
  290. buff_val = self.border_size_entry.get_value() * (self.bsize_entry.get_value() / 10)
  291. if self.bb_radio.get_value() == 'r':
  292. mask_geo = box(a, b, c, d).buffer(buff_val)
  293. else:
  294. mask_geo = box(a, b, c, d).buffer(buff_val, join_style=2)
  295. # update the solid geometry with the cutout (if it is the case)
  296. new_solid_geometry = list()
  297. offset_mask_geo = translate(mask_geo, xoff=pos[0], yoff=pos[1])
  298. for poly in geo_list:
  299. if poly.contains(offset_mask_geo):
  300. new_solid_geometry.append(poly.difference(offset_mask_geo))
  301. else:
  302. if poly not in new_solid_geometry:
  303. new_solid_geometry.append(poly)
  304. geo_list = deepcopy(list(new_solid_geometry))
  305. # Polarity
  306. if self.pol_radio.get_value() == 'pos':
  307. working_geo = self.qrcode_utility_geometry
  308. else:
  309. working_geo = mask_geo.difference(self.qrcode_utility_geometry)
  310. try:
  311. for geo in working_geo:
  312. geo_list.append(translate(geo, xoff=pos[0], yoff=pos[1]))
  313. except TypeError:
  314. geo_list.append(translate(working_geo, xoff=pos[0], yoff=pos[1]))
  315. self.grb_object.solid_geometry = deepcopy(geo_list)
  316. box_size = float(self.bsize_entry.get_value()) / 10.0
  317. sort_apid = list()
  318. new_apid = '10'
  319. if self.grb_object.apertures:
  320. for k, v in list(self.grb_object.apertures.items()):
  321. sort_apid.append(int(k))
  322. sorted_apertures = sorted(sort_apid)
  323. max_apid = max(sorted_apertures)
  324. if max_apid >= 10:
  325. new_apid = str(max_apid + 1)
  326. else:
  327. new_apid = '10'
  328. # don't know if the condition is required since I already made sure above that the new_apid is a new one
  329. if new_apid not in self.grb_object.apertures:
  330. self.grb_object.apertures[new_apid] = dict()
  331. self.grb_object.apertures[new_apid]['geometry'] = list()
  332. self.grb_object.apertures[new_apid]['type'] = 'R'
  333. # TODO: HACK
  334. # I've artificially added 1% to the height and width because otherwise after loading the
  335. # exported file, it will not be correctly reconstructed (it will be made from multiple shapes instead of
  336. # one shape which show that the buffering didn't worked well). It may be the MM to INCH conversion.
  337. self.grb_object.apertures[new_apid]['height'] = deepcopy(box_size * 1.01)
  338. self.grb_object.apertures[new_apid]['width'] = deepcopy(box_size * 1.01)
  339. self.grb_object.apertures[new_apid]['size'] = deepcopy(math.sqrt(box_size ** 2 + box_size ** 2))
  340. if '0' not in self.grb_object.apertures:
  341. self.grb_object.apertures['0'] = dict()
  342. self.grb_object.apertures['0']['geometry'] = list()
  343. self.grb_object.apertures['0']['type'] = 'REG'
  344. self.grb_object.apertures['0']['size'] = 0.0
  345. # in case that the QRCode geometry is dropped onto a copper region (found in the '0' aperture)
  346. # make sure that I place a cutout there
  347. zero_elem = dict()
  348. zero_elem['clear'] = offset_mask_geo
  349. self.grb_object.apertures['0']['geometry'].append(deepcopy(zero_elem))
  350. try:
  351. a, b, c, d = self.grb_object.bounds()
  352. self.grb_object.options['xmin'] = a
  353. self.grb_object.options['ymin'] = b
  354. self.grb_object.options['xmax'] = c
  355. self.grb_object.options['ymax'] = d
  356. except Exception as e:
  357. log.debug("QRCode.make() bounds error --> %s" % str(e))
  358. try:
  359. for geo in self.qrcode_geometry:
  360. geo_elem = dict()
  361. geo_elem['solid'] = translate(geo, xoff=pos[0], yoff=pos[1])
  362. geo_elem['follow'] = translate(geo.centroid, xoff=pos[0], yoff=pos[1])
  363. self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
  364. except TypeError:
  365. geo_elem = dict()
  366. geo_elem['solid'] = self.qrcode_geometry
  367. self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
  368. # update the source file with the new geometry:
  369. self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'], filename=None,
  370. local_use=self.grb_object, use_thread=False)
  371. self.replot()
  372. def draw_utility_geo(self, pos):
  373. # face = '#0000FF' + str(hex(int(0.2 * 255)))[2:]
  374. outline = '#0000FFAF'
  375. offset_geo = list()
  376. # I use the len of self.qrcode_geometry instead of the utility one because the complexity of the polygons is
  377. # better seen in this
  378. if len(self.qrcode_geometry) <= 330:
  379. try:
  380. for poly in self.qrcode_utility_geometry:
  381. offset_geo.append(translate(poly.exterior, xoff=pos[0], yoff=pos[1]))
  382. for geo_int in poly.interiors:
  383. offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
  384. except TypeError:
  385. offset_geo.append(translate(self.qrcode_utility_geometry.exterior, xoff=pos[0], yoff=pos[1]))
  386. for geo_int in self.qrcode_utility_geometry.interiors:
  387. offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
  388. else:
  389. offset_geo = [translate(self.box_poly, xoff=pos[0], yoff=pos[1])]
  390. for shape in offset_geo:
  391. self.shapes.add(shape, color=outline, update=True, layer=0, tolerance=None)
  392. if self.app.is_legacy is True:
  393. self.shapes.redraw()
  394. def delete_utility_geo(self):
  395. self.shapes.clear(update=True)
  396. self.shapes.redraw()
  397. def on_mouse_move(self, event):
  398. if self.app.is_legacy is False:
  399. event_pos = event.pos
  400. else:
  401. event_pos = (event.xdata, event.ydata)
  402. try:
  403. x = float(event_pos[0])
  404. y = float(event_pos[1])
  405. except TypeError:
  406. return
  407. pos_canvas = self.app.plotcanvas.translate_coords((x, y))
  408. # if GRID is active we need to get the snapped positions
  409. if self.app.grid_status() == True:
  410. pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
  411. else:
  412. pos = pos_canvas
  413. dx = pos[0] - self.origin[0]
  414. dy = pos[1] - self.origin[1]
  415. # delete the utility geometry
  416. self.delete_utility_geo()
  417. self.draw_utility_geo((dx, dy))
  418. def on_mouse_release(self, event):
  419. # mouse click will be accepted only if the left button is clicked
  420. # this is necessary because right mouse click and middle mouse click
  421. # are used for panning on the canvas
  422. if self.app.is_legacy is False:
  423. event_pos = event.pos
  424. else:
  425. event_pos = (event.xdata, event.ydata)
  426. if event.button == 1:
  427. pos_canvas = self.app.plotcanvas.translate_coords(event_pos)
  428. self.delete_utility_geo()
  429. # if GRID is active we need to get the snapped positions
  430. if self.app.grid_status() == True:
  431. pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
  432. else:
  433. pos = pos_canvas
  434. dx = pos[0] - self.origin[0]
  435. dy = pos[1] - self.origin[1]
  436. self.make(pos=(dx, dy))
  437. def on_key_release(self, event):
  438. pass
  439. def convert_svg_to_geo(self, filename, object_type=None, flip=True, units='MM'):
  440. """
  441. Convert shapes from an SVG file into a geometry list.
  442. :param filename: A String Stream file.
  443. :param object_type: parameter passed further along. What kind the object will receive the SVG geometry
  444. :param flip: Flip the vertically.
  445. :type flip: bool
  446. :param units: FlatCAM units
  447. :return: None
  448. """
  449. # Parse into list of shapely objects
  450. svg_tree = ET.parse(filename)
  451. svg_root = svg_tree.getroot()
  452. # Change origin to bottom left
  453. # h = float(svg_root.get('height'))
  454. # w = float(svg_root.get('width'))
  455. h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet
  456. geos = getsvggeo(svg_root, object_type)
  457. if flip:
  458. geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
  459. # flatten the svg geometry for the case when the QRCode SVG is added into a Gerber object
  460. solid_geometry = list(self.flatten_list(geos))
  461. geos_text = getsvgtext(svg_root, object_type, units=units)
  462. if geos_text is not None:
  463. geos_text_f = []
  464. if flip:
  465. # Change origin to bottom left
  466. for i in geos_text:
  467. _, minimy, _, maximy = i.bounds
  468. h2 = (maximy - minimy) * 0.5
  469. geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2)))
  470. if geos_text_f:
  471. solid_geometry += geos_text_f
  472. return solid_geometry
  473. def flatten_list(self, geo_list):
  474. for item in geo_list:
  475. if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
  476. yield from self.flatten_list(item)
  477. else:
  478. yield item
  479. def replot(self):
  480. obj = self.grb_object
  481. def worker_task():
  482. with self.app.proc_container.new('%s...' % _("Plotting")):
  483. obj.plot()
  484. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  485. def on_exit(self):
  486. if self.app.is_legacy is False:
  487. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  488. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  489. self.app.plotcanvas.graph_event_disconnect('key_release', self.on_key_release)
  490. else:
  491. self.app.plotcanvas.graph_event_disconnect(self.mm)
  492. self.app.plotcanvas.graph_event_disconnect(self.mr)
  493. self.app.plotcanvas.graph_event_disconnect(self.kr)
  494. # delete the utility geometry
  495. self.delete_utility_geo()
  496. self.app.call_source = 'app'