Merged in Beta_8.995_ali (pull request #21)

Fixed TCL Cutout and GeoCutout and added support for newer gap options

Approved-by: Marius Stanciu
This commit is contained in:
Ali Khalil
2022-04-06 09:19:52 +00:00
committed by Marius Stanciu
3 changed files with 82 additions and 68 deletions

5
.gitignore vendored
View File

@@ -3,3 +3,8 @@
tests/tmp/ tests/tmp/
build/ build/
/venv/ /venv/
# General for macOS
.DS_Store
.AppleDouble
.LSOverride

View File

@@ -1,28 +1,32 @@
# from matplotlib.colors import LinearSegmentedColormap
from tclCommands.TclCommand import TclCommand from tclCommands.TclCommand import TclCommand
import collections import collections
import logging import logging
from copy import deepcopy from copy import deepcopy
import gettext
from shapely.ops import unary_union from shapely.geometry import LineString, box
from shapely.geometry import LineString from shapely.ops import linemerge
from camlib import flatten_shapely_geometry
log = logging.getLogger('base') log = logging.getLogger('base')
class TclCommandCutout(TclCommand): class TclCommandCutout(TclCommand):
""" """
Tcl shell command to create a board cutout geometry. Rectangular shape only. Tcl shell command to create a board cutout geometry.
example: example:
cutout cut_object -dia 1.2 -margin 0.1 -gapsize 1 -gaps "tb" -outname cutout_geo -type rect
""" """
# List of all command aliases, to be able use old # List of all command aliases, to be able use old
# names for backward compatibility (add_poly, add_polygon) # names for backward compatibility (add_poly, add_polygon)
aliases = ['cutout'] aliases = ['cutout', 'geocutout']
description = '%s %s' % ("--", "Creates board cutout from an object (Gerber or Geometry) with a rectangular shape.") description = '%s %s' % ("--", "Creates board cutout from an object (Gerber or Geometry).")
# Dictionary of types from Tcl command, needs to be ordered # Dictionary of types from Tcl command, needs to be ordered
arg_names = collections.OrderedDict([ arg_names = collections.OrderedDict([
@@ -32,28 +36,31 @@ class TclCommandCutout(TclCommand):
# Dictionary of types from Tcl command, needs to be ordered, # Dictionary of types from Tcl command, needs to be ordered,
# this is for options like -optionname value # this is for options like -optionname value
option_types = collections.OrderedDict([ option_types = collections.OrderedDict([
('type', str),
('dia', float), ('dia', float),
('margin', float), ('margin', float),
('gapsize', float), ('gapsize', float),
('gaps', str), ('gaps', str),
('outname', str) ('outname', str)
]) ])
# array of mandatory options for current Tcl command: required = {'name','outname'} # array of mandatory options for current Tcl command: required = {'name','outname'}
required = ['name'] required = ['name']
# structured help for current command, args needs to be ordered # structured help for current command, args needs to be ordered
help = { help = {
'main': 'Creates board cutout from an object (Gerber or Geometry) with a rectangular shape.', 'main': 'Creates board cutout from an object (Gerber or Geometry).',
'args': collections.OrderedDict([ 'args': collections.OrderedDict([
('name', 'Name of the object.'), ('name', 'Name of the object.'),
('type', "Type of cutout. Can be: 'rect' or 'any'. default: any"),
('dia', 'Tool diameter.'), ('dia', 'Tool diameter.'),
('margin', 'Margin over bounds.'), ('margin', 'Margin over bounds.'),
('gapsize', 'Size of gap.'), ('gapsize', 'Size of gap.'),
('gaps', "Type of gaps. Can be: 'tb' = top-bottom, 'lr' = left-right and '4' = one each side."), ('gaps', "Type of gaps. Can be (case-insensitive): 'None' = no-tabs, 'TB' = top-bottom, 'LR' = left-right, "
"'2TB' = 2-top-bottom, '2LR' = 2-left-right, '4' = one each side, and '8' = two each side."),
('outname', 'Name of the object to create.') ('outname', 'Name of the object to create.')
]), ]),
'examples': ['cutout cut_object -dia 1.2 -margin 0.1 -gapsize 1 -gaps "tb" -outname cutout_geo'] 'examples': ['cutout cut_object -dia 1.2 -margin 0.1 -gapsize 1 -gaps "tb" -outname cutout_geo -type rect']
} }
def execute(self, args, unnamed_args): def execute(self, args, unnamed_args):
@@ -67,7 +74,7 @@ class TclCommandCutout(TclCommand):
if 'name' in args: if 'name' in args:
name = args['name'] name = args['name']
else: else:
self.app.inform.emit( self.app.log.warning(
"[WARNING] The name of the object for which cutout is done is missing. Add it and retry.") "[WARNING] The name of the object for which cutout is done is missing. Add it and retry.")
return "fail" return "fail"
@@ -82,9 +89,9 @@ class TclCommandCutout(TclCommand):
dia_par = float(self.app.options["tools_cutout_tooldia"]) dia_par = float(self.app.options["tools_cutout_tooldia"])
if 'gaps' in args: if 'gaps' in args:
if args['gaps'] not in ["tb", "lr", "4", 4]: if str(args['gaps']).lower() not in ["none", "tb", "lr", "2tb", "2lr", "4", "8"]:
self.raise_tcl_error( self.raise_tcl_error(
"Incorrect -gaps values. Can be only a string from: 'tb', 'lr' and '4'.") "Incorrect -gaps values. Can be only a string from: 'none', 'tb', 'lr', '2tb', '2lr', '4', and '8'.")
return "fail" return "fail"
gaps_par = str(args['gaps']) gaps_par = str(args['gaps'])
else: else:
@@ -100,8 +107,17 @@ class TclCommandCutout(TclCommand):
else: else:
outname = name + "_cutout" outname = name + "_cutout"
if 'type' in args:
if args['type'] not in ['rect', 'any']:
self.raise_tcl_error(_("Incorrect -type value. Can only an be: 'rect', 'any'. default: any"))
return 'fail'
type_par = args['type']
else:
self.app.log.info(_("No type value specified. Using default: any."))
type_par = 'any'
try: try:
obj = self.app.collection.get_by_name(str(name)) cutout_obj = self.app.collection.get_by_name(str(name))
except Exception as e: except Exception as e:
self.app.log.error("TclCommandCutout.execute(). Missing object: --> %s" % str(e)) self.app.log.error("TclCommandCutout.execute(). Missing object: --> %s" % str(e))
self.app.log.debug("Could not retrieve object: %s" % name) self.app.log.debug("Could not retrieve object: %s" % name)
@@ -109,49 +125,41 @@ class TclCommandCutout(TclCommand):
def geo_init_me(geo_obj, app_obj): def geo_init_me(geo_obj, app_obj):
geo_obj.multigeo = True geo_obj.multigeo = True
solid_geo = []
margin = margin_par + dia_par / 2 gapsize = gapsize_par + dia_par
gap_size = dia_par + gapsize_par
minx, miny, maxx, maxy = obj.bounds() if type_par == 'rect':
minx -= margin xmin, ymin, xmax, ymax = cutout_obj.bounds()
maxx += margin cutout_geom = flatten_shapely_geometry(box(xmin, ymin, xmax, ymax))
miny -= margin else:
maxy += margin cutout_geom = flatten_shapely_geometry(cutout_obj.solid_geometry)
midx = 0.5 * (minx + maxx)
midy = 0.5 * (miny + maxy)
hgap = 0.5 * gap_size
pts = [[midx - hgap, maxy],
[minx, maxy],
[minx, midy + hgap],
[minx, midy - hgap],
[minx, miny],
[midx - hgap, miny],
[midx + hgap, miny],
[maxx, miny],
[maxx, midy - hgap],
[maxx, midy + hgap],
[maxx, maxy],
[midx + hgap, maxy]]
cases = { for geom_struct in cutout_geom:
"tb": [ if cutout_obj.kind == 'gerber':
[pts[0], pts[1], pts[4], pts[5]], if margin_par >= 0:
[pts[6], pts[7], pts[10], pts[11]] geom_struct = (geom_struct.buffer(margin_par + abs(dia_par / 2))).exterior
], else:
"lr": [ geom_struct_buff = geom_struct.buffer(-margin_par + abs(dia_par / 2))
[pts[9], pts[10], pts[1], pts[2]], geom_struct = geom_struct_buff.interiors
[pts[3], pts[4], pts[7], pts[8]]
], if type_par == 'rect':
"4": [ solid_geo = self.app.cutout_tool.rect_cutout_handler(geom_struct, dia_par, gaps_par, gapsize, margin_par, xmin, ymin, xmax, ymax)
[pts[0], pts[1], pts[2]], else:
[pts[3], pts[4], pts[5]], solid_geo, r_geo = self.app.cutout_tool.any_cutout_handler(geom_struct, dia_par, gaps_par, gapsize, margin_par)
[pts[6], pts[7], pts[8]],
[pts[9], pts[10], pts[11]] if not solid_geo:
] self.app.log.debug("TclCommandCutout.geo_init_me() -> Empty solid geometry.")
} self.app.log.error('[ERROR] %s' % _("Failed."))
cuts = cases[gaps_par] return "fail"
geo_obj.solid_geometry = unary_union([LineString(segment) for segment in cuts])
try:
solid_geo = linemerge(solid_geo)
except Exception:
# there are not lines but polygon
pass
geo_obj.solid_geometry = solid_geo
if not geo_obj.solid_geometry: if not geo_obj.solid_geometry:
app_obj.log("TclCommandCutout.execute(). No geometry after cutout.") app_obj.log("TclCommandCutout.execute(). No geometry after cutout.")
@@ -176,7 +184,7 @@ class TclCommandCutout(TclCommand):
if ret == 'fail': if ret == 'fail':
self.app.log.error("Could not create a cutout Geometry object." ) self.app.log.error("Could not create a cutout Geometry object." )
return "fail" return "fail"
self.app.inform.emit("[success] Rectangular-form Cutout operation finished.") self.app.log.info("[success] Cutout operation finished.")
except Exception as e: except Exception as e:
self.app.log.error("Cutout operation failed: %s" % str(e)) self.app.log.error("Cutout operation failed: %s" % str(e))
return "fail" return "fail"

View File

@@ -29,7 +29,7 @@ class TclCommandGeoCutout(TclCommandSignaled):
# List of all command aliases, to be able use old # List of all command aliases, to be able use old
# names for backward compatibility (add_poly, add_polygon) # names for backward compatibility (add_poly, add_polygon)
aliases = ['geocutout', 'geoc'] aliases = ['geoc']
description = '%s %s' % ("--", "Creates board cutout from an object (Gerber or Geometry) of any shape.") description = '%s %s' % ("--", "Creates board cutout from an object (Gerber or Geometry) of any shape.")
@@ -59,8 +59,8 @@ class TclCommandGeoCutout(TclCommandSignaled):
('dia', 'Tool diameter.'), ('dia', 'Tool diameter.'),
('margin', 'Margin over bounds.'), ('margin', 'Margin over bounds.'),
('gapsize', 'size of gap.'), ('gapsize', 'size of gap.'),
('gaps', "type of gaps. Can be: 'tb' = top-bottom, 'lr' = left-right, '2tb' = 2top-2bottom, " ('gaps', "type of gaps. Can be: 'None' = no-gaps, 'TB' = top-bottom, 'LR' = left-right, '2TB' = 2top-2bottom, "
"'2lr' = 2left-2right, '4' = 4 cuts, '8' = 8 cuts"), "'2LR' = 2left-2right, '4' = 4 cuts, '8' = 8 cuts"),
('outname', 'Name of the resulting Geometry object.'), ('outname', 'Name of the resulting Geometry object.'),
]), ]),
'examples': [" #isolate margin for example from Fritzing arduino shield or any svg etc\n" + 'examples': [" #isolate margin for example from Fritzing arduino shield or any svg etc\n" +
@@ -140,7 +140,7 @@ class TclCommandGeoCutout(TclCommandSignaled):
name = args['name'] name = args['name']
else: else:
msg = "[WARNING] %s" % _("The name of the object for which cutout is done is missing. Add it and retry.") msg = "[WARNING] %s" % _("The name of the object for which cutout is done is missing. Add it and retry.")
self.app.inform.emit(msg) self.app.log.warning(msg)
return "fail" return "fail"
if 'margin' in args: if 'margin' in args:
@@ -177,13 +177,14 @@ class TclCommandGeoCutout(TclCommandSignaled):
return "fail" return "fail"
if 0 in {dia}: if 0 in {dia}:
self.app.inform.emit( self.app.log.warning(
"[WARNING] %s" % _("Tool Diameter is zero value. Change it to a positive real number.")) "[WARNING] %s" % _("Tool Diameter is zero value. Change it to a positive real number."))
return "fail" return "fail"
if gaps not in ['lr', 'tb', '2lr', '2tb', '4', '8', 4, 8]: if str(gaps).lower() not in ['none', 'lr', 'tb', '2lr', '2tb', '4', '8']:
self.app.inform.emit( self.app.log.warning('[WARNING] %s' %
"[WARNING] %s" % _("Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8.")) _("Gaps value can be only one of: 'none', 'lr', 'tb', '2lr', '2tb', 4 or 8.\n"
"Fill in a correct value and retry."))
return "fail" return "fail"
# Get min and max data for each object as we just cut rectangles across X or Y # Get min and max data for each object as we just cut rectangles across X or Y
@@ -214,14 +215,14 @@ class TclCommandGeoCutout(TclCommandSignaled):
self.app.log.error("TclCommandGeoCutout.execute() --> %s" % str(exc)) self.app.log.error("TclCommandGeoCutout.execute() --> %s" % str(exc))
return 'fail' return 'fail'
else: else:
self.app.inform.emit("[ERROR] %s" % _("Cancelled. Object type is not supported.")) self.app.log.error("[ERROR] %s" % _("Cancelled. Object type is not supported."))
return "fail" return "fail"
def geo_init(geo_obj, app_obj): def geo_init(geo_obj, app_obj):
geo_obj.multigeo = True geo_obj.multigeo = True
geo = geo_to_cutout geo = geo_to_cutout
if gaps_u == 8 or gaps_u == '2lr': if gaps_u == 8 or gaps_u == '2LR':
geo = substract_rectangle_geo(geo, geo = substract_rectangle_geo(geo,
xmin - gapsize, # botleft_x xmin - gapsize, # botleft_x
py - gapsize + lenghty / 4, # botleft_y py - gapsize + lenghty / 4, # botleft_y
@@ -233,7 +234,7 @@ class TclCommandGeoCutout(TclCommandSignaled):
xmax + gapsize, xmax + gapsize,
py + gapsize - lenghty / 4) py + gapsize - lenghty / 4)
if gaps_u == 8 or gaps_u == '2tb': if gaps_u == 8 or gaps_u == '2TB':
geo = substract_rectangle_geo(geo, geo = substract_rectangle_geo(geo,
px - gapsize + lenghtx / 4, px - gapsize + lenghtx / 4,
ymin - gapsize, ymin - gapsize,
@@ -245,14 +246,14 @@ class TclCommandGeoCutout(TclCommandSignaled):
px + gapsize - lenghtx / 4, px + gapsize - lenghtx / 4,
ymax + gapsize) ymax + gapsize)
if gaps_u == 4 or gaps_u == 'lr': if gaps_u == 4 or gaps_u == 'LR':
geo = substract_rectangle_geo(geo, geo = substract_rectangle_geo(geo,
xmin - gapsize, xmin - gapsize,
py - gapsize, py - gapsize,
xmax + gapsize, xmax + gapsize,
py + gapsize) py + gapsize)
if gaps_u == 4 or gaps_u == 'tb': if gaps_u == 4 or gaps_u == 'TB':
geo = substract_rectangle_geo(geo, geo = substract_rectangle_geo(geo,
px - gapsize, px - gapsize,
ymin - gapsize, ymin - gapsize,
@@ -291,4 +292,4 @@ class TclCommandGeoCutout(TclCommandSignaled):
self.app.log.error(msg) self.app.log.error(msg)
return "fail" return "fail"
else: else:
self.app.inform.emit("[success] %s" % _("Any-form Cutout operation finished.")) self.app.log.info("[success] %s" % _("Any-form Cutout operation finished."))