~freecad-community/freecad-extras/lattice2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
#***************************************************************************
#*                                                                         *
#*   Copyright (c) 2015 - Victor Titov (DeepSOIC)                          *
#*                                               <vv.titov@gmail.com>      *  
#*                                                                         *
#*   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 vaule are rejected by the filter.")
        obj.WindowFrom = 0.0
        obj.addProperty("App::PropertyLength","WindowTo","Lattice ArrayFilter","Elements farther from stencil than this vaule 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 independece 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 exctract 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 --------------------------------------------------