From 39c9e2a451d8ec281dd7b9b998f335e85590d37c Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Thu, 8 Oct 2020 18:16:24 -0500 Subject: [PATCH] Part: update icon, object and command for CompoundFilter The previous icon did not follow the general style and colors of other icons in the workbench. If there is no `Stencil` in the `CompoundFilter` object, raise a `ValueError` exception when the `FilterType` is `'collision-pass'` or `'window-distance'`. Raise an exception when `items` is empty, or has a malformed string, when `Filtertype` is `'specific items'`. Fix the `getNullShapeShape` function to return a simple `Part.Shape`, so that there is no error raised if `_nullShapeShape` doesn't exist. This function doesn't work at all. It was probably a prototype which was never fully developed; it may be removed completely in the future. The docstrings for the commands `CompoundFilter` and `ExplodeCompound` were revised. Clean up the spacing of the code so that lines aren't very long. --- src/Mod/Part/CompoundTools/CompoundFilter.py | 117 +++- .../CompoundTools/_CommandCompoundFilter.py | 13 +- .../CompoundTools/_CommandExplodeCompound.py | 6 +- .../icons/booleans/Part_CompoundFilter.svg | 635 +++++++++++++----- 4 files changed, 571 insertions(+), 200 deletions(-) diff --git a/src/Mod/Part/CompoundTools/CompoundFilter.py b/src/Mod/Part/CompoundTools/CompoundFilter.py index 96d835f74b..b97f1f0710 100644 --- a/src/Mod/Part/CompoundTools/CompoundFilter.py +++ b/src/Mod/Part/CompoundTools/CompoundFilter.py @@ -34,7 +34,8 @@ import sys if sys.version_info.major >= 3: xrange = range -# OCC's Precision::Confusion; should have taken this from FreeCAD but haven't found; unlikely to ever change (DeepSOIC) +# OCC's Precision::Confusion; should have taken this from FreeCAD +# but haven't found; unlikely to ever change (DeepSOIC) DistConfusion = 1e-7 ParaConfusion = 1e-8 @@ -54,38 +55,55 @@ def makeCompoundFilter(name, into_group = None): class _CompoundFilter: - "The CompoundFilter object" + """The CompoundFilter object.""" + def __init__(self, obj): obj.addProperty("App::PropertyLink", "Base", "CompoundFilter", "Compound to be filtered") - obj.addProperty("App::PropertyEnumeration", "FilterType", "CompoundFilter", "") - obj.FilterType = ['bypass', 'specific items', 'collision-pass', 'window-volume', 'window-area', 'window-length', 'window-distance'] + obj.addProperty("App::PropertyEnumeration", + "FilterType", + "CompoundFilter", + "Type of filter method to use; some of these methods " + "require, or are affected by, a 'Stencil' object.") + obj.FilterType = ['bypass', 'specific items', 'collision-pass', + 'window-volume', 'window-area', 'window-length', + 'window-distance'] obj.FilterType = 'bypass' # properties controlling "specific items" mode - obj.addProperty("App::PropertyString", "items", "CompoundFilter", "list of indexes of childs to be returned (like this: 1;4;8:10).") + obj.addProperty("App::PropertyString", "items", "CompoundFilter", + "Indices of the pieces to be returned.\n" + "These are numbers separated by a semicolon, '1;3;5'.\n" + "A range can also be provided using a colon, '1;4;8:10'.") - obj.addProperty("App::PropertyLink", "Stencil", "CompoundFilter", "Object that defines filtering") + obj.addProperty("App::PropertyLink", "Stencil", "CompoundFilter", + "Object that defines filtering") - obj.addProperty("App::PropertyFloat", "WindowFrom", "CompoundFilter", "Value of threshold, expressed as a percentage of maximum value.") + obj.addProperty("App::PropertyFloat", "WindowFrom", "CompoundFilter", + "Value of threshold, expressed as a percentage of maximum value.") obj.WindowFrom = 80.0 - obj.addProperty("App::PropertyFloat", "WindowTo", "CompoundFilter", "Value of threshold, expressed as a percentage of maximum value.") + obj.addProperty("App::PropertyFloat", "WindowTo", "CompoundFilter", + "Value of threshold, expressed as a percentage of maximum value.") obj.WindowTo = 100.0 - obj.addProperty("App::PropertyFloat", "OverrideMaxVal", "CompoundFilter", "Volume threshold, expressed as percentage of the volume of largest child") + obj.addProperty("App::PropertyFloat", "OverrideMaxVal", "CompoundFilter", + "Volume threshold, expressed as percentage of the volume of largest child") obj.OverrideMaxVal = 0 - obj.addProperty("App::PropertyBool", "Invert", "CompoundFilter", "Output shapes that are rejected by filter, instead") + obj.addProperty("App::PropertyBool", "Invert", "CompoundFilter", + "Output shapes that are rejected by the filter, instead") obj.Invert = False self.Type = "CompoundFilter" obj.Proxy = self def execute(self, obj): - # When operating on the object, it is to be treated as a lattice object. If False, treat as a regular shape.''' + # When operating on the object, it is to be treated as a lattice object. + # If False, treat as a regular shape. if hasattr(obj, "isLattice"): if 'On' in obj.isLattice: - print(obj.Name + " A generic shape is expected, but an array of placements was supplied. It will be treated as a generic shape.\n") + print(obj.Name + " A generic shape is expected, but an array of placements was supplied. " + "It will be treated as a generic shape.\n") rst = [] # variable to receive the final list of shapes shps = obj.Base.Shape.childShapes() @@ -94,16 +112,28 @@ class _CompoundFilter: elif obj.FilterType == 'specific items': rst = [] flags = [False] * len(shps) + if not obj.items: + raise ValueError("The 'items' property must have a number to use this filter: '{}'".format(obj.FilterType)) ranges = obj.items.split(';') for r in ranges: r_v = r.split(':') if len(r_v) == 1: - i = int(r_v[0]) - rst.append(shps[i]) + try: + i = int(r_v[0]) + except ValueError: + raise ValueError("Make sure the 'item' does not have spaces; " + "a semicolon (;) can only appear between two numbers; " + "filter: '{}'".format(obj.FilterType)) + try: + rst.append(shps[i]) + except IndexError: + raise ValueError("Item index '{}' is out of range for this filter: '{}'".format(i, obj.FilterType)) flags[i] = True elif len(r_v) == 2 or len(r_v) == 3: if len(r_v) == 2: - r_v.append("") # fix issue #1: instead of checking length here and there, simply add the missing field =) (DeepSOIC) + # fix issue #1: instead of checking length + # here and there, simply add the missing field =) (DeepSOIC) + r_v.append("") ifrom = None if len(r_v[0].strip()) == 0 else int(r_v[0]) ito = None if len(r_v[1].strip()) == 0 else int(r_v[1]) istep = None if len(r_v[2].strip()) == 0 else int(r_v[2]) @@ -111,19 +141,23 @@ class _CompoundFilter: for b in flags[ifrom:ito:istep]: b = True else: - raise ValueError('index range cannot be parsed:' + r) + raise ValueError("index range cannot be parsed: '{}'".format(r)) if obj.Invert: rst = [] for i in xrange(0, len(shps)): if not flags[i]: rst.append(shps[i]) elif obj.FilterType == 'collision-pass': + if not obj.Stencil: + raise ValueError("A 'Stencil' object must be set to use this filter: '{}'".format(obj.FilterType)) + stencil = obj.Stencil.Shape for s in shps: d = s.distToShape(stencil) if bool(d[0] < DistConfusion) ^ bool(obj.Invert): rst.append(s) - elif obj.FilterType == 'window-volume' or obj.FilterType == 'window-area' or obj.FilterType == 'window-length' or obj.FilterType == 'window-distance': + elif obj.FilterType in ('window-volume', 'window-area', + 'window-length', 'window-distance'): vals = [0.0] * len(shps) for i in xrange(0, len(shps)): if obj.FilterType == 'window-volume': @@ -133,6 +167,8 @@ class _CompoundFilter: elif obj.FilterType == 'window-length': vals[i] = shps[i].Length elif obj.FilterType == 'window-distance': + if not obj.Stencil: + raise ValueError("A 'Stencil' object must be set to use this filter: '{}'".format(obj.FilterType)) vals[i] = shps[i].distToShape(obj.Stencil.Shape)[0] maxval = max(vals) @@ -153,7 +189,7 @@ class _CompoundFilter: if bool(vals[i] >= valFrom and vals[i] <= valTo) ^ obj.Invert: rst.append(shps[i]) else: - raise ValueError('Filter mode not implemented:' + obj.FilterType) + raise ValueError("Filter mode not implemented: '{}'".format(obj.FilterType)) if len(rst) == 0: scale = 1.0 @@ -163,7 +199,9 @@ class _CompoundFilter: scale = 1.0 print(scale) obj.Shape = getNullShapeShape(scale) - raise ValueError('Nothing passes through the filter') # Feeding empty compounds to FreeCAD seems to cause rendering issues, otherwise it would have been a good idea to output nothing. + # Feeding empty compounds to FreeCAD seems to cause rendering issues, + # otherwise it would have been a good idea to output nothing. + raise ValueError('Nothing passes through the filter') if len(rst) > 1: obj.Shape = Part.makeCompound(rst) @@ -177,11 +215,15 @@ class _CompoundFilter: class _ViewProviderCompoundFilter: - "A View Provider for the CompoundFilter object" + """A View Provider for the CompoundFilter object.""" def __init__(self, vobj): vobj.Proxy = self - vobj.addProperty("App::PropertyBool", "DontUnhideOnDelete", "CompoundFilter", "When this object is deleted, Base and Stencil are unhidden. This flag stops it from happening.") + vobj.addProperty("App::PropertyBool", + "DontUnhideOnDelete", + "CompoundFilter", + "When this object is deleted, Base and Stencil are unhidden. " + "This flag stops it from happening.") vobj.setEditorMode("DontUnhideOnDelete", 2) # set hidden def getIcon(self): @@ -203,7 +245,8 @@ class _ViewProviderCompoundFilter: children.append(self.Object.Stencil) return children - def onDelete(self, feature, subelements): # subelements is a tuple of strings + def onDelete(self, feature, subelements): + """subelements is a tuple of strings.""" if not self.ViewObject.DontUnhideOnDelete: try: if self.Object.Base: @@ -221,23 +264,35 @@ class _ViewProviderCompoundFilter: return True -# helper def getNullShapeShape(scale=1.0): - """obtains a shape intended ad a placeholder in case null shape was produced by an operation""" - + """Obtains a shape intended as a placeholder in case null shape was produced by an operation""" # read shape from file, if not done this before + global _nullShapeShape - if not _nullShapeShape: + try: + # TODO: check this, and possibly remove this code + # vocx: this doesn't work at all. + # What is `empty-shape.brep`? An empty shape that is imported + # from a BREP file? Why do we need it? What does it contain? + # It does not even exist. + # The simplest way to return a `Null` shape is with `Part.Shape`. + if not _nullShapeShape: + _nullShapeShape = Part.Shape() + import os + shapePath = os.path.join(os.path.dirname(__file__), + "shapes", "empty-shape.brep") + f = open(shapePath) + _nullShapeShape.importBrep(f) + f.close() + except NameError: _nullShapeShape = Part.Shape() - import os - shapePath = os.path.dirname(__file__) + os.path.sep + "shapes" + os.path.sep + "empty-shape.brep" - f = open(shapePath) - _nullShapeShape.importBrep(f) - f.close() # scale the shape ret = _nullShapeShape if scale != 1.0: ret = _nullShapeShape.copy() + + # In the past the software would crash trying to scale a Null shape ret.scale(scale) + return ret diff --git a/src/Mod/Part/CompoundTools/_CommandCompoundFilter.py b/src/Mod/Part/CompoundTools/_CommandCompoundFilter.py index 2e50d6e160..e2611b6caa 100644 --- a/src/Mod/Part/CompoundTools/_CommandCompoundFilter.py +++ b/src/Mod/Part/CompoundTools/_CommandCompoundFilter.py @@ -56,7 +56,13 @@ class _CommandCompoundFilter: return {'Pixmap': "Part_CompoundFilter", 'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_CompoundFilter", "Compound Filter"), 'Accel': "", - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_CompoundFilter", "Compound Filter: remove some childs from a compound")} + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_CompoundFilter", + "Filter out objects from a selected compound " + "by characteristics like volume,\n" + "area, or length, or by choosing specific items.\n" + "If a second object is selected, it will be used " + "as reference, for example,\n" + "for collision or distance filtering.")} def Activated(self): if len(FreeCADGui.Selection.getSelection()) == 1 or len(FreeCADGui.Selection.getSelection()) == 2: @@ -64,7 +70,10 @@ class _CommandCompoundFilter: else: mb = QtGui.QMessageBox() mb.setIcon(mb.Icon.Warning) - mb.setText(_translate("Part_CompoundFilter", "Select a shape that is a compound, first! Second selected item (optional) will be treated as a stencil.", None)) + mb.setText(_translate("Part_CompoundFilter", + "First select a shape that is a compound. " + "If a second object is selected (optional) " + "it will be treated as a stencil.", None)) mb.setWindowTitle(_translate("Part_CompoundFilter", "Bad selection", None)) mb.exec_() diff --git a/src/Mod/Part/CompoundTools/_CommandExplodeCompound.py b/src/Mod/Part/CompoundTools/_CommandExplodeCompound.py index 8485548c3a..11afd7fa15 100644 --- a/src/Mod/Part/CompoundTools/_CommandExplodeCompound.py +++ b/src/Mod/Part/CompoundTools/_CommandExplodeCompound.py @@ -55,7 +55,9 @@ class _CommandExplodeCompound: return {'Pixmap': "Part_ExplodeCompound", 'MenuText': QtCore.QT_TRANSLATE_NOOP("Part_ExplodeCompound", "Explode compound"), 'Accel': "", - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_ExplodeCompound", "Explode compound: split up a list of shapes into separate objects")} + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Part_ExplodeCompound", + "Split up a compound of shapes into separate objects.\n" + "It will create a 'Compound Filter' for each shape.")} def Activated(self): if len(FreeCADGui.Selection.getSelection()) == 1: @@ -63,7 +65,7 @@ class _CommandExplodeCompound: else: mb = QtGui.QMessageBox() mb.setIcon(mb.Icon.Warning) - mb.setText(_translate("Part_ExplodeCompound", "Select a shape that is a compound, first!", None)) + mb.setText(_translate("Part_ExplodeCompound", "First select a shape that is a compound.", None)) mb.setWindowTitle(_translate("Part_ExplodeCompound", "Bad selection", None)) mb.exec_() diff --git a/src/Mod/Part/Gui/Resources/icons/booleans/Part_CompoundFilter.svg b/src/Mod/Part/Gui/Resources/icons/booleans/Part_CompoundFilter.svg index 09c2a8128c..1ff8e3802b 100644 --- a/src/Mod/Part/Gui/Resources/icons/booleans/Part_CompoundFilter.svg +++ b/src/Mod/Part/Gui/Resources/icons/booleans/Part_CompoundFilter.svg @@ -1,6 +1,4 @@ - - + id="svg2568" + height="64px" + width="64px"> + Part_CompoundFilter + id="stop3866" /> + id="stop3868" /> - + id="stop3379" /> + id="stop3381" /> + id="stop3025" /> + id="stop3027" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + xlink:href="#linearGradient4287" + id="linearGradient4293" + x1="25.559771" + y1="48.403759" + x2="31.477535" + y2="52.710686" + gradientUnits="userSpaceOnUse" /> + + style="stop-color:#f87c71;stop-opacity:1;" /> + style="stop-color:#ff0000;stop-opacity:1;" /> + + + + + + + + + + + + + + + + - @@ -178,104 +469,118 @@ image/svg+xml - + Part_CompoundFilter + + + + Bernd Hahnebach + + + 2020-10-08 + + + [vocx] + + + + + CC-BY-SA 4.0 + + + + + FreeCAD + + + FreeCAD/src/Mod/Part/Gui/Resources/icons/Part_CompoundFilter.svg + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + Cube + component + funnel + + + Based on the 'Part_Compound' icon, a cube is superimposed on a funnel, indicating that it is being filtered. + + + + + + + + + id="layer1"> + + - - - - - - - - - - - - + transform="translate(-0.51898211,0.20824558)" + id="g972"> + transform="matrix(0.56155482,0,0,0.56000041,15.316722,1.0813807)" + id="g3004-2" + style="stroke-width:1.41834235"> + id="path2993-5" + d="M 3,13 37,19 61,11 31,7 Z" + style="fill:#729fcf;stroke:#0b1521;stroke-width:3.56647968;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" /> + id="path2995-4" + d="M 61,11 V 47 L 37,57 V 19 Z" + style="fill:url(#linearGradient1905);fill-opacity:1;stroke:#0b1521;stroke-width:3.56647968;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" /> + style="display:inline;overflow:visible;visibility:visible;fill:url(#linearGradient1907);fill-opacity:1;fill-rule:evenodd;stroke:#0b1521;stroke-width:3.56647968;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" + d="m 3,13 34,6 V 57 L 3,51 Z" + id="path3825-8-7" /> + + + + + + +