- QRCode Tool: added ability to add negative QRCodes (perhaps they can be isolated on copper?); added a clear area surrounding the QRCode in case it is dropped on a copper pour (region); fixed the Gerber export

- QRCode Tool: all parameters are hard-coded for now
This commit is contained in:
Marius Stanciu 2019-10-25 01:20:52 +03:00 committed by Marius
parent ee61ba63fa
commit dfb8d21d1c
3 changed files with 197 additions and 93 deletions

View File

@ -9,12 +9,17 @@ CAD program, and create G-Code for Isolation routing.
================================================= =================================================
25.10.2019
- QRCode Tool: added ability to add negative QRCodes (perhaps they can be isolated on copper?); added a clear area surrounding the QRCode in case it is dropped on a copper pour (region); fixed the Gerber export
- QRCode Tool: all parameters are hard-coded for now
24.10.2019 24.10.2019
- added some placeholder texts in the TextBoxes. - added some placeholder texts in the TextBoxes.
- working on QRCode Tool; addded the utility geometry and intial functional layout - working on QRCode Tool; added the utility geometry and intial functional layout
- working on QRCode Tool; finished adding the QRCode geometry to the selected Gerber object and also finished adding the 'follow' geometry needed when exporting the Gerber object as a Gerber file in addition to the 'solid' geometry in the obj.apertures - working on QRCode Tool; finished adding the QRCode geometry to the selected Gerber object and also finished adding the 'follow' geometry needed when exporting the Gerber object as a Gerber file in addition to the 'solid' geometry in the obj.apertures
- working on QRCode Tool; finished offseting the goemetry both in apertures and in solid_geometry; updated the source_file of the source object - working on QRCode Tool; finished offseting the geometry both in apertures and in solid_geometry; updated the source_file of the source object
23.10.2019 23.10.2019
@ -27,7 +32,7 @@ CAD program, and create G-Code for Isolation routing.
- working on the Calibrate Excellon Tool - working on the Calibrate Excellon Tool
- finished the GUI layout for the Calibrate Excellon Tool - finished the GUI layout for the Calibrate Excellon Tool
- start working on QRCode Tool - not working yet - start working on QRCode Tool - not working yet
- start working on QRCode Tool - serching for alternatives - start working on QRCode Tool - searching for alternatives
21.10.2019 21.10.2019

View File

@ -3451,6 +3451,30 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
# Jump to coords # Jump to coords
if key == QtCore.Qt.Key_J or key == 'J': if key == QtCore.Qt.Key_J or key == 'J':
self.app.on_jump_to() self.app.on_jump_to()
elif self.app.call_source == 'qrcode_tool':
if modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.AltModifier:
if key == QtCore.Qt.Key_X:
self.app.abort_all_tasks()
return
elif modifiers == QtCore.Qt.ControlModifier:
pass
elif modifiers == QtCore.Qt.ShiftModifier:
pass
elif modifiers == QtCore.Qt.AltModifier:
pass
elif modifiers == QtCore.Qt.NoModifier:
# Escape = Deselect All
if key == QtCore.Qt.Key_Escape or key == 'Escape':
self.app.qrcode_tool.on_exit()
# Grid toggle
if key == QtCore.Qt.Key_G:
self.app.ui.grid_snap_btn.trigger()
# Jump to coords
if key == QtCore.Qt.Key_J:
self.app.on_jump_to()
def createPopupMenu(self): def createPopupMenu(self):
menu = super().createPopupMenu() menu = super().createPopupMenu()

View File

@ -11,19 +11,19 @@ from FlatCAMTool import FlatCAMTool
from flatcamGUI.GUIElements import RadioSet, FCTextArea, FCSpinner, FCDoubleSpinner from flatcamGUI.GUIElements import RadioSet, FCTextArea, FCSpinner, FCDoubleSpinner
from flatcamParsers.ParseSVG import * from flatcamParsers.ParseSVG import *
from shapely.geometry import Point
from shapely.geometry.base import * from shapely.geometry.base import *
from shapely.ops import unary_union from shapely.ops import unary_union
from shapely.affinity import translate from shapely.affinity import translate
from shapely.geometry import box
from io import StringIO, BytesIO from io import StringIO, BytesIO
from collections import Iterable from collections import Iterable
import logging import logging
from copy import deepcopy
import qrcode import qrcode
import qrcode.image.svg import qrcode.image.svg
from lxml import etree as ET from lxml import etree as ET
from copy import copy, deepcopy
from numpy import Inf
import gettext import gettext
import FlatCAMTranslation as fcTranslate import FlatCAMTranslation as fcTranslate
@ -147,7 +147,7 @@ class QRCode(FlatCAMTool):
self.border_size_label = QtWidgets.QLabel('%s:' % _("Border Size")) self.border_size_label = QtWidgets.QLabel('%s:' % _("Border Size"))
self.border_size_label.setToolTip( self.border_size_label.setToolTip(
_("Size of the QRCode border. How many boxes thick is the border.\n" _("Size of the QRCode border. How many boxes thick is the border.\n"
"Default value is 4.") "Default value is 4. The width of the clearance around the QRCode.")
) )
self.border_size_entry = FCSpinner() self.border_size_entry = FCSpinner()
self.border_size_entry.set_range(1, 9999) self.border_size_entry.set_range(1, 9999)
@ -172,15 +172,13 @@ class QRCode(FlatCAMTool):
# POLARITY CHOICE # # POLARITY CHOICE #
self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity")) self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity"))
self.pol_label.setToolTip( self.pol_label.setToolTip(
_("Parameter that controls the error correction used for the QR Code.\n" _("Choose the polarity of the QRCode.\n"
"L = maximum 7% errors can be corrected\n" "It can be drawn in a negative way (squares are clear)\n"
"M = maximum 15% errors can be corrected\n" "or in a positive way (squares are opaque).")
"Q = maximum 25% errors can be corrected\n"
"H = maximum 30% errors can be corrected.")
) )
self.pol_radio = RadioSet([{'label': _('Negative'), 'value': 'neg'}, self.pol_radio = RadioSet([{'label': _('Negative'), 'value': 'neg'},
{'label': _('Positive'), 'value': 'pos'}]) {'label': _('Positive'), 'value': 'pos'}])
self.error_radio.setToolTip( self.pol_radio.setToolTip(
_("Choose the type of QRCode to be created.\n" _("Choose the type of QRCode to be created.\n"
"If added on a Silkscreen Gerber you may add\n" "If added on a Silkscreen Gerber you may add\n"
"it as positive. If you add it to a Copper\n" "it as positive. If you add it to a Copper\n"
@ -189,18 +187,20 @@ class QRCode(FlatCAMTool):
grid_lay.addWidget(self.pol_label, 7, 0) grid_lay.addWidget(self.pol_label, 7, 0)
grid_lay.addWidget(self.pol_radio, 7, 1) grid_lay.addWidget(self.pol_radio, 7, 1)
# BOUNDARY THICKNESS # # BOUNDING BOX TYPE #
self.boundary_label = QtWidgets.QLabel('%s:' % _("Boundary Thickness")) self.bb_label = QtWidgets.QLabel('%s:' % _("Bounding Box"))
self.boundary_label.setToolTip( self.bb_label.setToolTip(
_("The width of the clearance around the QRCode.") _("The bounding box, meaning the empty space that surrounds\n"
"the QRCode geometry, can have a rounded or a square shape.")
) )
self.boundary_entry = FCDoubleSpinner() self.bb_radio = RadioSet([{'label': _('Rounded'), 'value': 'r'},
self.boundary_entry.set_range(0.0, 9999.9999) {'label': _('Square'), 'value': 's'}])
self.boundary_entry.set_precision(self.decimals) self.bb_radio.setToolTip(
self.boundary_entry.setWrapping(True) _("The bounding box, meaning the empty space that surrounds\n"
"the QRCode geometry, can have a rounded or a square shape.")
grid_lay.addWidget(self.boundary_label, 8, 0) )
grid_lay.addWidget(self.boundary_entry, 8, 1) grid_lay.addWidget(self.bb_label, 8, 0)
grid_lay.addWidget(self.bb_radio, 8, 1)
# ## Create QRCode # ## Create QRCode
self.qrcode_button = QtWidgets.QPushButton(_("Create QRCode")) self.qrcode_button = QtWidgets.QPushButton(_("Create QRCode"))
@ -213,6 +213,8 @@ class QRCode(FlatCAMTool):
self.layout.addStretch() self.layout.addStretch()
self.grb_object = None self.grb_object = None
self.box_poly = None
self.proc = None
self.origin = (0, 0) self.origin = (0, 0)
@ -221,8 +223,8 @@ class QRCode(FlatCAMTool):
self.kr = None self.kr = None
self.shapes = self.app.move_tool.sel_shapes self.shapes = self.app.move_tool.sel_shapes
self.qrcode_geometry = list() self.qrcode_geometry = MultiPolygon()
self.qrcode_utility_geometry = list() self.qrcode_utility_geometry = MultiPolygon()
def run(self, toggle=True): def run(self, toggle=True):
self.app.report_usage("QRCode()") self.app.report_usage("QRCode()")
@ -262,73 +264,79 @@ class QRCode(FlatCAMTool):
self.bsize_entry.set_value(3) self.bsize_entry.set_value(3)
self.border_size_entry.set_value(4) self.border_size_entry.set_value(4)
self.pol_radio.set_value('pos') self.pol_radio.set_value('pos')
self.bb_radio.set_value('r')
# Signals # # Signals #
self.qrcode_button.clicked.connect(self.execute) self.qrcode_button.clicked.connect(self.execute)
def execute(self): def execute(self):
text_data = self.text_data.get_value() text_data = self.text_data.get_value()
if text_data == '': if text_data == '':
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box.")) self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
return 'fail' return 'fail'
error_code = { # get the Gerber object on which the QRCode will be inserted
'L': qrcode.constants.ERROR_CORRECT_L, selection_index = self.grb_object_combo.currentIndex()
'M': qrcode.constants.ERROR_CORRECT_M, model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
'Q': qrcode.constants.ERROR_CORRECT_Q,
'H': qrcode.constants.ERROR_CORRECT_H
}[self.error_radio.get_value()]
qr = qrcode.QRCode( try:
version=self.version_entry.get_value(), self.grb_object = model_index.internalPointer().obj
error_correction=error_code, except Exception as e:
box_size=self.bsize_entry.get_value(), log.debug("QRCode.execute() --> %s" % str(e))
border=self.border_size_entry.get_value(), self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
image_factory=qrcode.image.svg.SvgFragmentImage return 'fail'
)
qr.add_data(text_data)
qr.make()
svg_file = BytesIO() # we can safely activate the mouse events
img = qr.make_image()
img.save(svg_file)
svg_text = StringIO(svg_file.getvalue().decode('UTF-8'))
svg_geometry = self.convert_svg_to_geo(svg_text, units=self.units)
self.qrcode_geometry = deepcopy(svg_geometry)
svg_geometry = unary_union(svg_geometry).buffer(0.0000001).buffer(-0.0000001)
self.qrcode_utility_geometry = svg_geometry
# if we have an object selected then we can safely activate the mouse events
self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move) self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release) self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_release) self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_release)
selection_index = self.grb_object_combo.currentIndex() self.proc = self.app.proc_container.new('%s...' % _("Generating QRCode geometry"))
model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
try:
self.grb_object = model_index.internalPointer().obj
except Exception as e:
self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
return 'fail'
self.app.inform.emit(_("Click on the Destination point ...")) def job_thread_qr(app_obj):
error_code = {
'L': qrcode.constants.ERROR_CORRECT_L,
'M': qrcode.constants.ERROR_CORRECT_M,
'Q': qrcode.constants.ERROR_CORRECT_Q,
'H': qrcode.constants.ERROR_CORRECT_H
}[self.error_radio.get_value()]
qr = qrcode.QRCode(
version=self.version_entry.get_value(),
error_correction=error_code,
box_size=self.bsize_entry.get_value(),
border=self.border_size_entry.get_value(),
image_factory=qrcode.image.svg.SvgFragmentImage
)
qr.add_data(text_data)
qr.make()
svg_file = BytesIO()
img = qr.make_image()
img.save(svg_file)
svg_text = StringIO(svg_file.getvalue().decode('UTF-8'))
svg_geometry = self.convert_svg_to_geo(svg_text, units=self.units)
self.qrcode_geometry = deepcopy(svg_geometry)
svg_geometry = unary_union(svg_geometry).buffer(0.0000001).buffer(-0.0000001)
self.qrcode_utility_geometry = svg_geometry
# make a bounding box of the QRCode geometry to help drawing the utility geometry in case it is too
# complicated
try:
a, b, c, d = self.qrcode_utility_geometry.bounds
self.box_poly = box(minx=a, miny=b, maxx=c, maxy=d)
except Exception as e:
log.debug("QRCode.make() bounds error --> %s" % str(e))
app_obj.call_source = 'qrcode_tool'
app_obj.inform.emit(_("Click on the Destination point ..."))
self.app.worker_task.emit({'fcn': job_thread_qr, 'params': [self.app]})
def make(self, pos): def make(self, pos):
if self.app.is_legacy is False: self.on_exit()
self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
self.app.plotcanvas.graph_event_disconnect('key_release', self.on_key_release)
else:
self.app.plotcanvas.graph_event_disconnect(self.mm)
self.app.plotcanvas.graph_event_disconnect(self.mr)
self.app.plotcanvas.graph_event_disconnect(self.kr)
# delete the utility geometry
self.delete_utility_geo()
# add the svg geometry to the selected Gerber object solid_geometry and in obj.apertures, apid = 0 # add the svg geometry to the selected Gerber object solid_geometry and in obj.apertures, apid = 0
if not isinstance(self.grb_object.solid_geometry, Iterable): if not isinstance(self.grb_object.solid_geometry, Iterable):
@ -339,11 +347,38 @@ class QRCode(FlatCAMTool):
if isinstance(self.grb_object.solid_geometry, MultiPolygon): if isinstance(self.grb_object.solid_geometry, MultiPolygon):
geo_list = list(self.grb_object.solid_geometry.geoms) geo_list = list(self.grb_object.solid_geometry.geoms)
# this is the bounding box of the QRCode geometry
a, b, c, d = self.qrcode_utility_geometry.bounds
buff_val = self.border_size_entry.get_value() * (self.bsize_entry.get_value() / 10)
if self.bb_radio.get_value() == 'r':
mask_geo = box(a, b, c, d).buffer(buff_val)
else:
mask_geo = box(a, b, c, d).buffer(buff_val, join_style=2)
# update the solid geometry with the cutout (if it is the case)
new_solid_geometry = list()
offset_mask_geo = translate(mask_geo, xoff=pos[0], yoff=pos[1])
for poly in geo_list:
if poly.contains(offset_mask_geo):
new_solid_geometry.append(poly.difference(offset_mask_geo))
else:
if poly not in new_solid_geometry:
new_solid_geometry.append(poly)
geo_list = deepcopy(list(new_solid_geometry))
# Polarity
if self.pol_radio.get_value() == 'pos':
working_geo = self.qrcode_utility_geometry
else:
working_geo = mask_geo.difference(self.qrcode_utility_geometry)
try: try:
for geo in self.qrcode_utility_geometry: for geo in working_geo:
geo_list.append(translate(geo, xoff=pos[0], yoff=pos[1])) geo_list.append(translate(geo, xoff=pos[0], yoff=pos[1]))
except TypeError: except TypeError:
geo_list.append(translate(self.qrcode_utility_geometry, xoff=pos[0], yoff=pos[1])) geo_list.append(translate(working_geo, xoff=pos[0], yoff=pos[1]))
self.grb_object.solid_geometry = deepcopy(geo_list) self.grb_object.solid_geometry = deepcopy(geo_list)
@ -355,16 +390,37 @@ class QRCode(FlatCAMTool):
for k, v in list(self.grb_object.apertures.items()): for k, v in list(self.grb_object.apertures.items()):
sort_apid.append(int(k)) sort_apid.append(int(k))
sorted_apertures = sorted(sort_apid) sorted_apertures = sorted(sort_apid)
new_apid = str(max(sorted_apertures) + 1) max_apid = max(sorted_apertures)
if max_apid >= 10:
new_apid = str(max_apid + 1)
else:
new_apid = '10'
# don't know if the condition is required since I already made sure above that the new_apid is a new one
if new_apid not in self.grb_object.apertures: if new_apid not in self.grb_object.apertures:
self.grb_object.apertures[new_apid] = dict() self.grb_object.apertures[new_apid] = dict()
self.grb_object.apertures[new_apid]['geometry'] = list() self.grb_object.apertures[new_apid]['geometry'] = list()
self.grb_object.apertures[new_apid]['type'] = 'R' self.grb_object.apertures[new_apid]['type'] = 'R'
self.grb_object.apertures[new_apid]['height'] = deepcopy(box_size) # TODO: HACK
self.grb_object.apertures[new_apid]['width'] = deepcopy(box_size) # I've artificially added 1% to the height and width because otherwise after loading the
# exported file, it will not be correctly reconstructed (it will be made from multiple shapes instead of
# one shape which show that the buffering didn't worked well). It may be the MM to INCH conversion.
self.grb_object.apertures[new_apid]['height'] = deepcopy(box_size * 1.01)
self.grb_object.apertures[new_apid]['width'] = deepcopy(box_size * 1.01)
self.grb_object.apertures[new_apid]['size'] = deepcopy(math.sqrt(box_size ** 2 + box_size ** 2)) self.grb_object.apertures[new_apid]['size'] = deepcopy(math.sqrt(box_size ** 2 + box_size ** 2))
if '0' not in self.grb_object.apertures:
self.grb_object.apertures['0'] = dict()
self.grb_object.apertures['0']['geometry'] = list()
self.grb_object.apertures['0']['type'] = 'REG'
self.grb_object.apertures['0']['size'] = 0.0
# in case that the QRCode geometry is dropped onto a copper region (found in the '0' aperture)
# make sure that I place a cutout there
zero_elem = dict()
zero_elem['clear'] = offset_mask_geo
self.grb_object.apertures['0']['geometry'].append(deepcopy(zero_elem))
try: try:
a, b, c, d = self.grb_object.bounds() a, b, c, d = self.grb_object.bounds()
self.grb_object.options['xmin'] = a self.grb_object.options['xmin'] = a
@ -383,7 +439,7 @@ class QRCode(FlatCAMTool):
except TypeError: except TypeError:
geo_elem = dict() geo_elem = dict()
geo_elem['solid'] = self.qrcode_geometry geo_elem['solid'] = self.qrcode_geometry
self.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem)) self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
# update the source file with the new geometry: # update the source file with the new geometry:
self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'], filename=None, self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'], filename=None,
@ -393,29 +449,34 @@ class QRCode(FlatCAMTool):
def draw_utility_geo(self, pos): def draw_utility_geo(self, pos):
face = '#0000FF' + str(hex(int(0.2 * 255)))[2:] # face = '#0000FF' + str(hex(int(0.2 * 255)))[2:]
outline = '#0000FFAF' outline = '#0000FFAF'
offset_geo = list() offset_geo = list()
try: # I use the len of self.qrcode_geometry instead of the utility one because the complexity of the polygons is
for poly in self.qrcode_utility_geometry: # better seen in this
offset_geo.append(translate(poly.exterior, xoff=pos[0], yoff=pos[1])) if len(self.qrcode_geometry) <= 330:
for geo_int in poly.interiors: try:
for poly in self.qrcode_utility_geometry:
offset_geo.append(translate(poly.exterior, xoff=pos[0], yoff=pos[1]))
for geo_int in poly.interiors:
offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
except TypeError:
offset_geo.append(translate(self.qrcode_utility_geometry.exterior, xoff=pos[0], yoff=pos[1]))
for geo_int in self.qrcode_utility_geometry.interiors:
offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1])) offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
except TypeError: else:
offset_geo.append(translate(self.qrcode_utility_geometry.exterior, xoff=pos[0], yoff=pos[1])) offset_geo = [translate(self.box_poly, xoff=pos[0], yoff=pos[1])]
for geo_int in self.qrcode_utility_geometry.interiors:
offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
for shape in offset_geo: for shape in offset_geo:
self.shapes.add(shape, color=outline, face_color=face, update=True, layer=0, tolerance=None) self.shapes.add(shape, color=outline, update=True, layer=0, tolerance=None)
if self.app.is_legacy is True: if self.app.is_legacy is True:
self.shapes.redraw() self.shapes.redraw()
def delete_utility_geo(self): def delete_utility_geo(self):
self.shapes.clear() self.shapes.clear(update=True)
self.shapes.redraw() self.shapes.redraw()
def on_mouse_move(self, event): def on_mouse_move(self, event):
@ -514,8 +575,8 @@ class QRCode(FlatCAMTool):
solid_geometry += geos_text_f solid_geometry += geos_text_f
return solid_geometry return solid_geometry
def flatten_list(self, list): def flatten_list(self, geo_list):
for item in list: for item in geo_list:
if isinstance(item, Iterable) and not isinstance(item, (str, bytes)): if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
yield from self.flatten_list(item) yield from self.flatten_list(item)
else: else:
@ -529,3 +590,17 @@ class QRCode(FlatCAMTool):
obj.plot() obj.plot()
self.app.worker_task.emit({'fcn': worker_task, 'params': []}) self.app.worker_task.emit({'fcn': worker_task, 'params': []})
def on_exit(self):
if self.app.is_legacy is False:
self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
self.app.plotcanvas.graph_event_disconnect('key_release', self.on_key_release)
else:
self.app.plotcanvas.graph_event_disconnect(self.mm)
self.app.plotcanvas.graph_event_disconnect(self.mr)
self.app.plotcanvas.graph_event_disconnect(self.kr)
# delete the utility geometry
self.delete_utility_geo()
self.app.call_source = 'app'