#*************************************************************************** #* * #* Copyright (c) 2015 - Victor Titov (DeepSOIC) * #* * #* * #* This program is free software; you can redistribute it and/or modify * #* it under the terms of the GNU Lesser General Public License (LGPL) * #* as published by the Free Software Foundation; either version 2 of * #* the License, or (at your option) any later version. * #* for detail see the LICENCE text file. * #* * #* This program is distributed in the hope that it will be useful, * #* but WITHOUT ANY WARRANTY; without even the implied warranty of * #* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * #* GNU Library General Public License for more details. * #* * #* You should have received a copy of the GNU Library General Public * #* License along with this program; if not, write to the Free Software * #* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * #* USA * #* * #*************************************************************************** import FreeCAD as App import Part from lattice2Common import * import lattice2BaseFeature import lattice2CompoundExplorer as LCE import lattice2Executer __title__="Lattice ArrayFilter module for FreeCAD" __author__ = "DeepSOIC" __url__ = "" # -------------------------- common stuff -------------------------------------------------- def makeArrayFilter(name): '''makeArrayFilter(name): makes a Lattice ArrayFilter object.''' return lattice2BaseFeature.makeLatticeFeature(name, LatticeArrayFilter, ViewProviderArrayFilter) class LatticeArrayFilter(lattice2BaseFeature.LatticeFeature): "The Lattice ArrayFilter object" stencilModeList = ['collision-pass','window-distance', 'pointing-at'] def derivedInit(self,obj): self.Type = "LatticeArrayFilter" obj.addProperty("App::PropertyLink","Base","Lattice ArrayFilter","Array to be filtered") obj.addProperty("App::PropertyEnumeration","FilterType","Lattice ArrayFilter","") obj.FilterType = ['bypass','specific items']+LatticeArrayFilter.stencilModeList obj.FilterType = 'bypass' # properties controlling "specific items" mode obj.addProperty("App::PropertyString","items","Lattice ArrayFilter","list of indexes of items to be returned (like this: 1,4,8:10).") obj.addProperty("App::PropertyLink","Stencil","Lattice ArrayFilter","Object that defines filtering") obj.addProperty("App::PropertyLength","WindowFrom","Lattice ArrayFilter","Elements closer to stencil than this value are rejected by the filter.") obj.WindowFrom = 0.0 obj.addProperty("App::PropertyLength","WindowTo","Lattice ArrayFilter","Elements farther from stencil than this value are rejected by the filter.") obj.WindowTo = 1.0 obj.addProperty("App::PropertyBool","Invert","Lattice ArrayFilter","Output elements that are rejected by filter, instead") obj.Invert = False obj.Proxy = self def derivedExecute(self,obj): #validity check if not lattice2BaseFeature.isObjectLattice(screen(obj.Base)): lattice2Executer.warning(obj,"A lattice object is expected as Base, but a generic shape was provided. It will be treated as a lattice object; results may be unexpected.") output = [] #variable to receive the final list of placements leaves = LCE.AllLeaves(screen(obj.Base).Shape) input = [leaf.Placement for leaf in leaves] if obj.FilterType == 'bypass': output = input elif obj.FilterType == 'specific items': flags = [False] * len(input) ranges = obj.items.split(';') for r in ranges: r_v = r.split(':') if len(r_v) == 1: i = int(r_v[0]) output.append(input[i]) 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 =) 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]) output=output+input[ifrom:ito:istep] for b in flags[ifrom:ito:istep]: b = True else: raise ValueError('index range cannot be parsed:'+r) if obj.Invert : output = [] for i in range(0,len(input)): if not flags[i]: output.append(input[i]) elif obj.FilterType == 'collision-pass': stencil = screen(obj.Stencil).Shape for plm in input: pnt = Part.Vertex(plm.Base) d = pnt.distToShape(stencil) if bool(d[0] < DistConfusion) ^ bool(obj.Invert): output.append(plm) elif obj.FilterType == 'window-distance': vals = [0.0] * len(input) for i in range(0,len(input)): if obj.FilterType == 'window-distance': pnt = Part.Vertex(input[i].Base) vals[i] = pnt.distToShape(screen(obj.Stencil).Shape)[0] valFrom = obj.WindowFrom valTo = obj.WindowTo for i in range(0,len(input)): if bool(vals[i] >= valFrom and vals[i] <= valTo) ^ obj.Invert: output.append(input[i]) else: raise ValueError('Filter mode not implemented:'+obj.FilterType) return output class ViewProviderArrayFilter(lattice2BaseFeature.ViewProviderLatticeFeature): "A View Provider for the Lattice ArrayFilter object" def getIcon(self): return getIconPath("Lattice2_ArrayFilter.svg") def claimChildren(self): children = [screen(self.Object.Base)] if screen(self.Object.Stencil): children.append(screen(self.Object.Stencil)) return children def makeItemListFromSelection(sel, bMakeString = True): '''makeItemListFromSelection(sel, bMakeString = True): make a string for "items" property of ArrayFilter from selection object. sel should be a SelectionObject (e.g. Gui.Selection.getSelectionEx() returns a list of SelectionObjects) Returns a string like If bMakeString == False, the output will be a list of integers''' # figure out element counts of array marker for (child, msg, it) in LCE.CompoundExplorer(sel.Object.Shape): if msg == LCE.CompoundExplorer.MSG_LEAF: vertices_per_marker = len(child.Vertexes) edges_per_marker = len(child.Edges) faces_per_marker = len(child.Faces) break; # get indexes indexes = [] for sub in sel.SubElementNames: # figure out array element index of selected shape subelement if "Vertex" in sub: i_vert = int( sub[len("Vertex"):] ) - 1 i = int(i_vert/vertices_per_marker) elif "Edge" in sub: i_edge = int( sub[len("Edge"):] ) - 1 i = int(i_edge/edges_per_marker) elif "Face" in sub: i_face = int( sub[len("Face"):] ) - 1 i = int(i_face/faces_per_marker) # add the index to index list, avoiding duplicates if len(indexes) > 0 and i == indexes[-1]: pass else: indexes.append(i) if bMakeString: list_of_strings = [str(item) for item in indexes] return ';'.join(list_of_strings) else: return indexes def CreateLatticeArrayFilter(name,mode): sel = FreeCADGui.Selection.getSelectionEx() # selection order independence logic (lattice object and generic shape stencil can be told apart) iLtc = 0 #index of lattice object in selection iStc = 1 #index of stencil object in selection for i in range(0,len(sel)): if lattice2BaseFeature.isObjectLattice(sel[i]): iLtc = i iStc = i-1 #this may give negative index, but python accepts negative indexes break FreeCAD.ActiveDocument.openTransaction("Create ArrayFilter") FreeCADGui.addModule("lattice2ArrayFilter") FreeCADGui.addModule("lattice2Executer") FreeCADGui.doCommand("sel = Gui.Selection.getSelectionEx()") FreeCADGui.doCommand("f = lattice2ArrayFilter.makeArrayFilter(name = '"+name+"')") FreeCADGui.doCommand("f.Base = App.ActiveDocument."+sel[iLtc].ObjectName) FreeCADGui.doCommand("f.FilterType = '"+mode+"'") if mode == 'specific items': FreeCADGui.doCommand("f.items = lattice2ArrayFilter.makeItemListFromSelection(sel["+str(iLtc)+"])") if len(sel[0].SubElementNames) == 1: FreeCADGui.doCommand("f.ExposePlacement = True") else: FreeCADGui.doCommand("f.Stencil = App.ActiveDocument."+sel[iStc].ObjectName) FreeCADGui.doCommand("for child in f.ViewObject.Proxy.claimChildren():\n"+ " child.ViewObject.hide()") FreeCADGui.doCommand("lattice2Executer.executeFeature(f)") FreeCADGui.doCommand("f = None") FreeCAD.ActiveDocument.commitTransaction() # -------------------------- /common stuff -------------------------------------------------- # -------------------------- Gui command -------------------------------------------------- _listOfSubCommands = [] class _CommandArrayFilterItems: "Command to create Lattice ArrayFilter feature in 'specific items' mode based on current selection" def GetResources(self): return {'Pixmap' : getIconPath("Lattice2_ArrayFilter.svg"), 'MenuText': QtCore.QT_TRANSLATE_NOOP("Lattice2_ArrayFilter","Array Filter: selected items"), 'Accel': "", 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Lattice2_ArrayFilter","Array Filter: keep only items that are currently selected.")} def Activated(self): sel = FreeCADGui.Selection.getSelectionEx() if len(sel) == 1 and sel[0].HasSubObjects: CreateLatticeArrayFilter(name= "ArrayFilter", mode= 'specific items') else: mb = QtGui.QMessageBox() mb.setIcon(mb.Icon.Warning) mb.setText(translate("Lattice2_ArrayFilter", "Select elements of a lattice feature, first! Placements other than those that were selected are going to be rejected. The order of selection matters.", None)) mb.setWindowTitle(translate("Lattice2_ArrayFilter","Bad selection", None)) mb.exec_() def IsActive(self): if FreeCAD.ActiveDocument: return True else: return False if FreeCAD.GuiUp: FreeCADGui.addCommand('Lattice2_ArrayFilter_Items', _CommandArrayFilterItems()) _listOfSubCommands.append('Lattice2_ArrayFilter_Items') class _CommandArrayFilterStencilBased: "Command to create Lattice ArrayFilter feature in 'specific items' mode based on current selection" def __init__(self, mode): self.mode = mode def GetResources(self): return {'Pixmap' : getIconPath("Lattice2_ArrayFilter.svg"), 'MenuText': "Array Filter: " + {"collision-pass":"touching", "window-distance":"within distance window", "pointing-at":"pointing at shape"}[mode], 'Accel': "", 'ToolTip': {"collision-pass":"keep only placements that are on and/or in a stencil shape", "window-distance":"keep only placements that are within distance window to stencil shape", "pointing-at":"keep only placements whose X axis ray touches stencil object"}[mode]} def Activated(self): sel = FreeCADGui.Selection.getSelectionEx() if len(sel) == 2 : CreateLatticeArrayFilter(name= "ArrayFilter", mode= self.mode) else: mb = QtGui.QMessageBox() mb.setIcon(mb.Icon.Warning) mb.setText(translate("Lattice2_ArrayFilter", "Select a lattice array and a stencil shape, first!", None)) mb.setWindowTitle(translate("Lattice2_ArrayFilter","Bad selection", None)) mb.exec_() def IsActive(self): if FreeCAD.ActiveDocument: return True else: return False for mode in LatticeArrayFilter.stencilModeList: cmdName = 'Lattice2_ArrayFilter'+mode.replace("-","_") if FreeCAD.GuiUp: FreeCADGui.addCommand(cmdName, _CommandArrayFilterStencilBased(mode)) _listOfSubCommands.append(cmdName) class GroupCommandLatticeArrayFilter: def GetCommands(self): global _listOfSubCommands return tuple(_listOfSubCommands) # a tuple of command names that you want to group def GetDefaultCommand(self): # return the index of the tuple of the default command. This method is optional and when not implemented '0' is used return 0 def GetResources(self): return { 'MenuText': 'Array filter:', 'ToolTip': 'Array filter: tool to extract specific elements from lattice2 arrays.'} def IsActive(self): # optional return bool(App.ActiveDocument) if FreeCAD.GuiUp: FreeCADGui.addCommand('Lattice2_ArrayFilter_GroupCommand',GroupCommandLatticeArrayFilter()) def ExplodeArray(feature): plms = lattice2BaseFeature.getPlacementsList(feature) features_created = [] for i in range(len(plms)): af = makeArrayFilter(name = 'Placment') af.Label = u'Placement' + str(i) af.Base = feature af.FilterType = 'specific items' af.items = str(i) af.ExposePlacement = True af.ViewObject.DontUnhideOnDelete = True features_created.append(af) return features_created def cmdExplodeArray(): App.ActiveDocument.openTransaction("Explode array") try: sel = FreeCADGui.Selection.getSelectionEx() if len(sel) != 1: raise SelectionError("Bad selection","One array to be exploded must be selected. You have selected {num} objects.".format(num= len(sel))) obj = sel[0].Object FreeCADGui.addModule("lattice2ArrayFilter") FreeCADGui.addModule("lattice2Executer") FreeCADGui.doCommand("input_obj = App.ActiveDocument."+obj.Name) old_state = lattice2Executer.globalIsCreatingLatticeFeature lattice2Executer.globalIsCreatingLatticeFeature = True #for warnings to pop up FreeCADGui.doCommand("output_objs = lattice2ArrayFilter.ExplodeArray(input_obj)") lattice2Executer.globalIsCreatingLatticeFeature = old_state FreeCADGui.doCommand("\n".join([ "if len(output_objs) > 1:", " group = App.ActiveDocument.addObject('App::DocumentObjectGroup','GrExplode_'+input_obj.Name)", " group.Group = output_objs", " group.Label = 'Placements of '+input_obj.Label", " App.ActiveDocument.recompute()", "else:", " lattice2Executer.executeFeature(output_objs[0])", "", ]) ) FreeCADGui.doCommand("input_obj.ViewObject.hide()") except Exception: App.ActiveDocument.abortTransaction() lattice2Executer.globalIsCreatingLatticeFeature = False raise App.ActiveDocument.commitTransaction() class _CommandExplodeArray: "Command to explode array with parametric links to its elements" def GetResources(self): return {'Pixmap' : getIconPath("Lattice2_ExplodeArray.svg"), 'MenuText': QtCore.QT_TRANSLATE_NOOP("Lattice2_ArrayFilter","Explode array"), 'Accel': "", 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Lattice2_ArrayFilter","Explode array: get each element of array as a separate object")} def Activated(self): try: if len(FreeCADGui.Selection.getSelection()) > 0 : cmdExplodeArray() else: infoMessage("Explode array", "'Explode Array' command. Makes placements of array available as separate document objects.\n\n"+ "Select an object that is a Lattice array of placements, then invoke this command.") except Exception as err: msgError(err) def IsActive(self): if FreeCAD.ActiveDocument: return True else: return False if FreeCAD.GuiUp: FreeCADGui.addCommand('Lattice2_ExplodeArray', _CommandExplodeArray()) exportedCommands = ['Lattice2_ArrayFilter_GroupCommand', 'Lattice2_ExplodeArray'] # -------------------------- /Gui command --------------------------------------------------