- in Film Plugin added new parameters and improvements: now the negative film can have a box that is convex and it is no longer limited to square shapes. Also, if the box object has only one geometric element (an outline) then that one will be the final shape of the negative

This commit is contained in:
Marius Stanciu
2021-09-24 03:42:10 +03:00
committed by Marius
parent e7e7ab8664
commit 15b651147b
5 changed files with 144 additions and 42 deletions

View File

@@ -10,6 +10,7 @@ CHANGELOG for FlatCAM beta
24.09.2021 24.09.2021
- in Extract Plugin some minor UI changes - in Extract Plugin some minor UI changes
- in Film Plugin added new parameters and improvements: now the negative film can have a box that is convex and it is no longer limited to square shapes. Also, if the box object has only one geometric element (an outline) then that one will be the final shape of the negative
21.09.2021 21.09.2021

View File

@@ -473,6 +473,8 @@ class PreferencesUIManager:
"tools_2sided_allign_axis": self.ui.plugin_eng_pref_form.tools_2sided_group.align_axis_radio, "tools_2sided_allign_axis": self.ui.plugin_eng_pref_form.tools_2sided_group.align_axis_radio,
# Film Tool # Film Tool
"tools_film_shape": self.ui.plugin_pref_form.tools_film_group.convex_box_cb,
"tools_film_rounded": self.ui.plugin_pref_form.tools_film_group.rounded_cb,
"tools_film_polarity": self.ui.plugin_pref_form.tools_film_group.film_type_radio, "tools_film_polarity": self.ui.plugin_pref_form.tools_film_group.film_type_radio,
"tools_film_boundary": self.ui.plugin_pref_form.tools_film_group.film_boundary_entry, "tools_film_boundary": self.ui.plugin_pref_form.tools_film_group.film_boundary_entry,
"tools_film_scale_stroke": self.ui.plugin_pref_form.tools_film_group.film_scale_stroke_entry, "tools_film_scale_stroke": self.ui.plugin_pref_form.tools_film_group.film_scale_stroke_entry,

View File

@@ -184,14 +184,38 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
grid_par = FCGridLayout() grid_par = FCGridLayout()
par_frame.setLayout(grid_par) par_frame.setLayout(grid_par)
# Convex Shape
# Surrounding convex box shape
self.convex_box_label = FCLabel('%s:' % _("Convex Shape"))
self.convex_box_label.setToolTip(
_("Create a convex shape surrounding the entire PCB.\n"
"If not checked the shape is rectangular.")
)
self.convex_box_cb = FCCheckBox()
grid_par.addWidget(self.convex_box_label, 0, 0)
grid_par.addWidget(self.convex_box_cb, 0, 1)
# Rounded corners
self.rounded_label = FCLabel('%s:' % _("Rounded"))
self.rounded_label.setToolTip(
_("Resulting geometry will have rounded corners.")
)
self.rounded_cb = FCCheckBox()
grid_par.addWidget(self.rounded_label, 2, 0)
grid_par.addWidget(self.rounded_cb, 2, 1)
# Polarity
self.film_type_radio = RadioSet([{'label': 'Pos', 'value': 'pos'}, self.film_type_radio = RadioSet([{'label': 'Pos', 'value': 'pos'},
{'label': 'Neg', 'value': 'neg'}]) {'label': 'Neg', 'value': 'neg'}])
ftypelbl = FCLabel('%s:' % _('Polarity')) ftypelbl = FCLabel('%s:' % _('Polarity'))
ftypelbl.setToolTip( ftypelbl.setToolTip(
_("Generate a Positive black film or a Negative film.") _("Generate a Positive black film or a Negative film.")
) )
grid_par.addWidget(ftypelbl, 0, 0) grid_par.addWidget(ftypelbl, 4, 0)
grid_par.addWidget(self.film_type_radio, 0, 1) grid_par.addWidget(self.film_type_radio, 4, 1)
# Film Color # Film Color
self.film_color_label = FCLabel('%s:' % _('Film Color')) self.film_color_label = FCLabel('%s:' % _('Film Color'))
@@ -200,8 +224,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
) )
self.film_color_entry = FCColorEntry() self.film_color_entry = FCColorEntry()
grid_par.addWidget(self.film_color_label, 2, 0) grid_par.addWidget(self.film_color_label, 6, 0)
grid_par.addWidget(self.film_color_entry, 2, 1) grid_par.addWidget(self.film_color_entry, 6, 1)
# Film Border # Film Border
self.film_boundary_entry = FCDoubleSpinner() self.film_boundary_entry = FCDoubleSpinner()
@@ -220,9 +244,10 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
"white color like the rest and which may confound with the\n" "white color like the rest and which may confound with the\n"
"surroundings if not for this border.") "surroundings if not for this border.")
) )
grid_par.addWidget(self.film_boundary_label, 4, 0) grid_par.addWidget(self.film_boundary_label, 8, 0)
grid_par.addWidget(self.film_boundary_entry, 4, 1) grid_par.addWidget(self.film_boundary_entry, 8, 1)
# Scale Stroke
self.film_scale_stroke_entry = FCDoubleSpinner() self.film_scale_stroke_entry = FCDoubleSpinner()
self.film_scale_stroke_entry.set_precision(self.decimals) self.film_scale_stroke_entry.set_precision(self.decimals)
self.film_scale_stroke_entry.set_range(0, 10000.0000) self.film_scale_stroke_entry.set_range(0, 10000.0000)
@@ -234,8 +259,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
"It means that the line that envelope each SVG feature will be thicker or thinner,\n" "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
"therefore the fine features may be more affected by this parameter.") "therefore the fine features may be more affected by this parameter.")
) )
grid_par.addWidget(self.film_scale_stroke_label, 6, 0) grid_par.addWidget(self.film_scale_stroke_label, 10, 0)
grid_par.addWidget(self.film_scale_stroke_entry, 6, 1) grid_par.addWidget(self.film_scale_stroke_entry, 10, 1)
self.file_type_radio = RadioSet([{'label': _('SVG'), 'value': 'svg'}, self.file_type_radio = RadioSet([{'label': _('SVG'), 'value': 'svg'},
{'label': _('PNG'), 'value': 'png'}, {'label': _('PNG'), 'value': 'png'},
@@ -249,8 +274,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
"- 'PNG' -> raster image\n" "- 'PNG' -> raster image\n"
"- 'PDF' -> portable document format") "- 'PDF' -> portable document format")
) )
grid_par.addWidget(self.file_type_label, 8, 0) grid_par.addWidget(self.file_type_label, 12, 0)
grid_par.addWidget(self.file_type_radio, 8, 1) grid_par.addWidget(self.file_type_radio, 12, 1)
# Page orientation # Page orientation
self.orientation_label = FCLabel('%s:' % _("Page Orientation")) self.orientation_label = FCLabel('%s:' % _("Page Orientation"))
@@ -262,8 +287,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
{'label': _('Landscape'), 'value': 'l'}, {'label': _('Landscape'), 'value': 'l'},
], stretch=False) ], stretch=False)
grid_par.addWidget(self.orientation_label, 10, 0) grid_par.addWidget(self.orientation_label, 14, 0)
grid_par.addWidget(self.orientation_radio, 10, 1) grid_par.addWidget(self.orientation_radio, 14, 1)
# Page Size # Page Size
self.pagesize_label = FCLabel('%s:' % _("Page Size")) self.pagesize_label = FCLabel('%s:' % _("Page Size"))
@@ -328,8 +353,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
page_size_list = list(self.pagesize.keys()) page_size_list = list(self.pagesize.keys())
self.pagesize_combo.addItems(page_size_list) self.pagesize_combo.addItems(page_size_list)
grid_par.addWidget(self.pagesize_label, 12, 0) grid_par.addWidget(self.pagesize_label, 16, 0)
grid_par.addWidget(self.pagesize_combo, 12, 1) grid_par.addWidget(self.pagesize_combo, 16, 1)
# PNG DPI # PNG DPI
self.png_dpi_label = FCLabel('%s:' % "PNG DPI") self.png_dpi_label = FCLabel('%s:' % "PNG DPI")
@@ -339,8 +364,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
self.png_dpi_spinner = FCSpinner() self.png_dpi_spinner = FCSpinner()
self.png_dpi_spinner.set_range(0, 100000) self.png_dpi_spinner.set_range(0, 100000)
grid_par.addWidget(self.png_dpi_label, 14, 0) grid_par.addWidget(self.png_dpi_label, 18, 0)
grid_par.addWidget(self.png_dpi_spinner, 14, 1) grid_par.addWidget(self.png_dpi_spinner, 18, 1)
self.layout.addStretch(1) self.layout.addStretch(1)

View File

@@ -14,7 +14,7 @@ from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \
from copy import deepcopy from copy import deepcopy
import logging import logging
from shapely.geometry import Polygon, MultiPolygon, Point from shapely.geometry import Polygon, MultiPolygon, Point, LineString, LinearRing
import shapely.affinity as affinity import shapely.affinity as affinity
from shapely.ops import unary_union from shapely.ops import unary_union
@@ -211,6 +211,9 @@ class Film(AppTool):
self.ui.png_dpi_spinner.set_value(self.app.defaults["tools_film_png_dpi"]) self.ui.png_dpi_spinner.set_value(self.app.defaults["tools_film_png_dpi"])
self.ui.convex_box_cb.set_value(self.app.defaults["tools_film_shape"])
self.ui.rounded_cb.set_value(self.app.defaults["tools_film_rounded"])
obj = self.app.collection.get_active() obj = self.app.collection.get_active()
if obj: if obj:
obj_name = obj.options['name'] obj_name = obj.options['name']
@@ -483,6 +486,9 @@ class Film(AppTool):
def generate_negative_film(self, name, boxname, factor, ftype='svg'): def generate_negative_film(self, name, boxname, factor, ftype='svg'):
log.debug("ToolFilm.Film.generate_negative_film() started ...") log.debug("ToolFilm.Film.generate_negative_film() started ...")
use_convex_hull = self.ui.convex_box_cb.get_value()
rounded_box = self.ui.rounded_cb.get_value()
scale_factor_x = 1 scale_factor_x = 1
scale_factor_y = 1 scale_factor_y = 1
skew_factor_x = None skew_factor_x = None
@@ -546,7 +552,9 @@ class Film(AppTool):
scale_reference=scale_reference, scale_reference=scale_reference,
skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y, skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
skew_reference=skew_reference, skew_reference=skew_reference,
mirror=mirror, ftype=ftype mirror=mirror, ftype=ftype,
use_convex_hull=use_convex_hull,
rounded_box=rounded_box
) )
def export_negative(self, obj_name, box_name, filename, boundary, def export_negative(self, obj_name, box_name, filename, boundary,
@@ -554,7 +562,7 @@ class Film(AppTool):
scale_factor_x=1, scale_factor_y=1, scale_reference='center', scale_factor_x=1, scale_factor_y=1, scale_reference='center',
skew_factor_x=None, skew_factor_y=None, skew_reference='center', skew_factor_x=None, skew_factor_y=None, skew_reference='center',
mirror=None, opacity_val=1.0, mirror=None, opacity_val=1.0,
use_thread=True, ftype='svg'): use_thread=True, ftype='svg', use_convex_hull=False, rounded_box=False):
""" """
Exports a Geometry Object to an SVG file in negative. Exports a Geometry Object to an SVG file in negative.
@@ -576,6 +584,9 @@ class Film(AppTool):
:param opacity_val: :param opacity_val:
:param use_thread: if to be run in a separate thread; boolean :param use_thread: if to be run in a separate thread; boolean
:param ftype: the type of file for saving the film: 'svg', 'png' or 'pdf' :param ftype: the type of file for saving the film: 'svg', 'png' or 'pdf'
:param use_convex_hull: Bool; if True it will make the negative box to minimize the black coverage
:param rounded_box: Bool; if True the negative bounded box will have rounded corners
Works only in case the object used as box has multiple geometries
:return: :return:
""" """
self.app.defaults.report_usage("export_negative()") self.app.defaults.report_usage("export_negative()")
@@ -602,16 +613,16 @@ class Film(AppTool):
scale_factor_x = scale_factor_x scale_factor_x = scale_factor_x
scale_factor_y = scale_factor_y scale_factor_y = scale_factor_y
def get_complementary(color): def get_complementary(color_param):
# strip the # from the beginning # strip the # from the beginning
color = color[1:] our_color = color_param[1:]
# convert the string into hex # convert the string into hex
color = int(color, 16) our_color = int(our_color, 16)
# invert the three bytes # invert the three bytes
# as good as substracting each of RGB component by 255(FF) # as good as substracting each of RGB component by 255(FF)
comp_color = 0xFFFFFF ^ color comp_color = 0xFFFFFF ^ our_color
# convert the color back to hex by prefixing a # # convert the color back to hex by prefixing a #
comp_color = "#%06X" % comp_color comp_color = "#%06X" % comp_color
@@ -624,7 +635,7 @@ class Film(AppTool):
color = obj.options['tools_film_color'] color = obj.options['tools_film_color']
transparency_level = opacity_val transparency_level = opacity_val
def make_negative_film(color, transparency_level, scale_factor_x, scale_factor_y): def make_negative_film(color, transparency_level, scale_factor_x, scale_factor_y, use_convex_hull, rounded_box):
log.debug("FilmTool.export_negative().make_negative_film()") log.debug("FilmTool.export_negative().make_negative_film()")
self.screen_dpi = self.app.qapp.screens()[0].logicalDotsPerInch() self.screen_dpi = self.app.qapp.screens()[0].logicalDotsPerInch()
@@ -636,6 +647,8 @@ class Film(AppTool):
scale_factor_x += dpi_rate scale_factor_x += dpi_rate
scale_factor_y += dpi_rate scale_factor_y += dpi_rate
# ########################################################################################################
# the case when the BOX object is a Geometry Object
if box.kind.lower() == 'geometry': if box.kind.lower() == 'geometry':
flat_geo = [] flat_geo = []
if box.multigeo: if box.multigeo:
@@ -713,7 +726,7 @@ class Film(AppTool):
svgheight = str(size[1] + (2 * boundary)) svgheight = str(size[1] + (2 * boundary))
minx = str(bounds[0] - boundary) minx = str(bounds[0] - boundary)
miny = str(bounds[1] + boundary + size[1]) miny = str(bounds[1] + boundary + size[1])
miny_rect = str(bounds[1] - boundary) # miny_rect = str(bounds[1] - boundary)
# Add a SVG Header and footer to the svg output from shapely # Add a SVG Header and footer to the svg output from shapely
# The transform flips the Y Axis so that everything renders # The transform flips the Y Axis so that everything renders
@@ -741,12 +754,46 @@ class Film(AppTool):
# first_svg_elem += 'width="' + svgwidth + '" ' + 'height="' + svgheight + '" ' # first_svg_elem += 'width="' + svgwidth + '" ' + 'height="' + svgheight + '" '
# first_svg_elem += 'fill="#000000" opacity="1.0" stroke-width="0.0"' # first_svg_elem += 'fill="#000000" opacity="1.0" stroke-width="0.0"'
first_svg_elem_tag = 'rect' # first_svg_elem_tag = 'rect'
# first_svg_elem_attribs = {
# 'x': minx,
# 'y': miny_rect,
# 'width': svgwidth,
# 'height': svgheight,
# 'id': 'neg_rect',
# 'style': 'fill:%s;opacity:1.0;stroke-width:0.0' % str(color)
# }
# decide if to round the bounding box for the negative
join_s = 1 if rounded_box else 2
if isinstance(transformed_box_geo, (LineString, LinearRing)):
b_geo = Polygon(transformed_box_geo).buffer(boundary, join_style=join_s)
coords_list = list(b_geo.exterior.coords)
elif isinstance(transformed_box_geo, list) and len(transformed_box_geo) == 1 and \
isinstance(transformed_box_geo[0], (LineString, LinearRing)):
b_geo = Polygon(transformed_box_geo[0]).buffer(boundary, join_style=join_s)
coords_list = list(b_geo.exterior.coords)
elif isinstance(transformed_box_geo, Polygon):
coords_list = list(transformed_box_geo.exterior.coords)
elif isinstance(transformed_box_geo, list) and len(transformed_box_geo) == 1 and \
isinstance(transformed_box_geo[0], Polygon):
coords_list = list(transformed_box_geo[0].exterior.coords)
else:
if use_convex_hull:
buff_box = transformed_box_geo.convex_hull.buffer(boundary, join_style=join_s)
else:
buff_box = transformed_box_geo.envelope.buffer(boundary, join_style=join_s)
box_buff_outline = buff_box.exterior
coords_list = list(box_buff_outline.coords)
points_container = ''
for coord_tuple in coords_list:
points_container += '%s, %s ' % (str(coord_tuple[0]), str(coord_tuple[1]))
first_svg_elem_tag = 'polygon'
first_svg_elem_attribs = { first_svg_elem_attribs = {
'x': minx, 'points': points_container,
'y': miny_rect,
'width': svgwidth,
'height': svgheight,
'id': 'neg_rect', 'id': 'neg_rect',
'style': 'fill:%s;opacity:1.0;stroke-width:0.0' % str(color) 'style': 'fill:%s;opacity:1.0;stroke-width:0.0' % str(color)
} }
@@ -852,14 +899,16 @@ class Film(AppTool):
with self.app.proc_container.new(_("Working...")): with self.app.proc_container.new(_("Working...")):
try: try:
make_negative_film(color=color, transparency_level=transparency_level, make_negative_film(color=color, transparency_level=transparency_level,
scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y) scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
use_convex_hull=use_convex_hull, rounded_box=rounded_box)
except Exception as e: except Exception as e:
log.error("export_negative() process -> %s" % str(e)) log.error("export_negative() process -> %s" % str(e))
return return
self.app.worker_task.emit({'fcn': job_thread_film, 'params': []}) self.app.worker_task.emit({'fcn': job_thread_film, 'params': []})
else: else:
make_negative_film(scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y) make_negative_film(scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
use_convex_hull=use_convex_hull, rounded_box=rounded_box)
def export_positive(self, obj_name, box_name, filename, def export_positive(self, obj_name, box_name, filename,
scale_stroke_factor=0.00, scale_stroke_factor=0.00,
@@ -1435,6 +1484,29 @@ class FilmUI:
grid_par = FCGridLayout(v_spacing=5, h_spacing=3) grid_par = FCGridLayout(v_spacing=5, h_spacing=3)
par_frame.setLayout(grid_par) par_frame.setLayout(grid_par)
# Convex Shape
# Surrounding convex box shape
self.convex_box_label = FCLabel('%s:' % _("Convex Shape"))
self.convex_box_label.setToolTip(
_("Create a convex shape surrounding the entire PCB.\n"
"If not checked the shape is rectangular.")
)
self.convex_box_cb = FCCheckBox()
grid_par.addWidget(self.convex_box_label, 0, 0)
grid_par.addWidget(self.convex_box_cb, 0, 1)
# Rounded corners
self.rounded_label = FCLabel('%s:' % _("Rounded"))
self.rounded_label.setToolTip(
_("Resulting geometry will have rounded corners.")
)
self.rounded_cb = FCCheckBox()
grid_par.addWidget(self.rounded_label, 2, 0)
grid_par.addWidget(self.rounded_cb, 2, 1)
# Scale Stroke size # Scale Stroke size
self.film_scale_stroke_entry = FCDoubleSpinner(callback=self.confirmation_message) self.film_scale_stroke_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.film_scale_stroke_entry.set_range(-999.9999, 999.9999) self.film_scale_stroke_entry.set_range(-999.9999, 999.9999)
@@ -1447,10 +1519,10 @@ class FilmUI:
"It means that the line that envelope each SVG feature will be thicker or thinner,\n" "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
"therefore the fine features may be more affected by this parameter.") "therefore the fine features may be more affected by this parameter.")
) )
grid_par.addWidget(self.film_scale_stroke_label, 0, 0) grid_par.addWidget(self.film_scale_stroke_label, 4, 0)
grid_par.addWidget(self.film_scale_stroke_entry, 0, 1) grid_par.addWidget(self.film_scale_stroke_entry, 4, 1)
# Film Type # Polarity
self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'}, self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'},
{'label': _('Negative'), 'value': 'neg'}], {'label': _('Negative'), 'value': 'neg'}],
stretch=False) stretch=False)
@@ -1458,10 +1530,10 @@ class FilmUI:
self.film_type_label.setToolTip( self.film_type_label.setToolTip(
_("Generate a Positive black film or a Negative film.") _("Generate a Positive black film or a Negative film.")
) )
grid_par.addWidget(self.film_type_label, 2, 0) grid_par.addWidget(self.film_type_label, 6, 0)
grid_par.addWidget(self.film_type, 2, 1) grid_par.addWidget(self.film_type, 6, 1)
# Boundary for negative film generation # Border for negative film generation
self.boundary_entry = FCDoubleSpinner(callback=self.confirmation_message) self.boundary_entry = FCDoubleSpinner(callback=self.confirmation_message)
self.boundary_entry.set_range(-999.9999, 999.9999) self.boundary_entry.set_range(-999.9999, 999.9999)
self.boundary_entry.setSingleStep(0.01) self.boundary_entry.setSingleStep(0.01)
@@ -1478,8 +1550,8 @@ class FilmUI:
"white color like the rest and which may confound with the\n" "white color like the rest and which may confound with the\n"
"surroundings if not for this border.") "surroundings if not for this border.")
) )
grid_par.addWidget(self.boundary_label, 4, 0) grid_par.addWidget(self.boundary_label, 8, 0)
grid_par.addWidget(self.boundary_entry, 4, 1) grid_par.addWidget(self.boundary_entry, 8, 1)
self.boundary_label.hide() self.boundary_label.hide()
self.boundary_entry.hide() self.boundary_entry.hide()
@@ -1489,12 +1561,12 @@ class FilmUI:
self.punch_cb.setToolTip(_("When checked the generated film will have holes in pads when\n" self.punch_cb.setToolTip(_("When checked the generated film will have holes in pads when\n"
"the generated film is positive. This is done to help drilling,\n" "the generated film is positive. This is done to help drilling,\n"
"when done manually.")) "when done manually."))
grid_par.addWidget(self.punch_cb, 6, 0, 1, 2) grid_par.addWidget(self.punch_cb, 10, 0, 1, 2)
# this way I can hide/show the frame # this way I can hide/show the frame
self.punch_frame = QtWidgets.QFrame() self.punch_frame = QtWidgets.QFrame()
self.punch_frame.setContentsMargins(0, 0, 0, 0) self.punch_frame.setContentsMargins(0, 0, 0, 0)
grid_par.addWidget(self.punch_frame, 8, 0, 1, 2) grid_par.addWidget(self.punch_frame, 12, 0, 1, 2)
punch_grid = FCGridLayout(v_spacing=5, h_spacing=3) punch_grid = FCGridLayout(v_spacing=5, h_spacing=3)
punch_grid.setContentsMargins(0, 0, 0, 0) punch_grid.setContentsMargins(0, 0, 0, 0)

View File

@@ -542,6 +542,8 @@ class FlatCAMDefaults:
"tools_2sided_allign_axis": "X", "tools_2sided_allign_axis": "X",
# Film Tool # Film Tool
"tools_film_shape": False,
"tools_film_rounded": False,
"tools_film_polarity": 'neg', "tools_film_polarity": 'neg',
"tools_film_boundary": 1.0, "tools_film_boundary": 1.0,
"tools_film_scale_stroke": 0, "tools_film_scale_stroke": 0,