~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
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
#***************************************************************************
#*                                                                         *
#*   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                                                                   *
#*                                                                         *
#***************************************************************************


__title__="BoundingBox module for FreeCAD"
__author__ = "DeepSOIC"
__url__ = ""


from lattice2Common import *
import lattice2BaseFeature as LBF
import lattice2Executer as Executer
import lattice2CompoundExplorer as LCE

import FreeCAD as App

# -------------------------- common stuff --------------------------------------------------

def boundBox2RealBox(bb):
    base = FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMin)
    OX = FreeCAD.Vector(1, 0, 0)
    OY = FreeCAD.Vector(0, 1, 0)
    OZ = FreeCAD.Vector(0, 0, 1)
    if bb.XLength > DistConfusion and bb.YLength > DistConfusion and bb.ZLength > DistConfusion :
        return Part.makeBox(bb.XLength,bb.YLength,bb.ZLength, base, OZ)
    elif bb.XLength > DistConfusion and bb.YLength > DistConfusion:
        return Part.makePlane(bb.XLength, bb.YLength, base, OZ, OX)
    elif bb.XLength > DistConfusion and bb.ZLength > DistConfusion :
        return Part.makePlane(bb.XLength, bb.ZLength, base, OY*-1, OX)
    elif bb.YLength > DistConfusion and bb.ZLength > DistConfusion :
        return Part.makePlane(bb.YLength, bb.ZLength, base, OX, OY)
    elif bb.XLength > DistConfusion:
        return Part.makeLine(base, base + OX*bb.XLength)
    elif bb.YLength > DistConfusion:
        return Part.makeLine(base, base + OY*bb.YLength)
    elif bb.ZLength > DistConfusion:
        return Part.makeLine(base, base + OZ*bb.ZLength)
    else:
        raise ValueError("Bounding box is zero")
        
def scaledBoundBox(bb, scale):
    bb2 = FreeCAD.BoundBox(bb)
    cnt = bb.Center
    bb2.XMin = (bb.XMin - cnt.x)*scale + cnt.x
    bb2.YMin = (bb.YMin - cnt.y)*scale + cnt.y
    bb2.ZMin = (bb.ZMin - cnt.z)*scale + cnt.z
    bb2.XMax = (bb.XMax - cnt.x)*scale + cnt.x
    bb2.YMax = (bb.YMax - cnt.y)*scale + cnt.y
    bb2.ZMax = (bb.ZMax - cnt.z)*scale + cnt.z
    return bb2
    
def getPrecisionBoundBox(shape):
    # First, we need a box that for sure contains the object.
    # We use imprecise bound box, scaled up twice. The scaling
    # is required, because the imprecise bound box is often a
    # bit smaller than the shape.
    bb = scaledBoundBox(shape.BoundBox, 2.0)
    # Make sure bound box is not collapsed in any direction, 
    # to make sure boundBox2RealBox returns a box, not plane
    # or line
    if bb.XLength < DistConfusion or bb.YLength < DistConfusion or bb.ZLength < DistConfusion:
        bb.enlarge(1.0)
    
    # Make a boundingbox shape and compute distances from faces
    # of this enlarged bounding box to the actual shape. Shrink
    # the boundbox by the distances.
    bbshape = boundBox2RealBox(bb)
    #FIXME: it may be a good idea to not use hard-coded face indexes
    bb.XMin = bb.XMin + shape.distToShape(bbshape.Faces[0])[0]
    bb.YMin = bb.YMin + shape.distToShape(bbshape.Faces[2])[0]
    bb.ZMin = bb.ZMin + shape.distToShape(bbshape.Faces[4])[0]
    bb.XMax = bb.XMax - shape.distToShape(bbshape.Faces[1])[0]
    bb.YMax = bb.YMax - shape.distToShape(bbshape.Faces[3])[0]
    bb.ZMax = bb.ZMax - shape.distToShape(bbshape.Faces[5])[0]
    return bb


def makeBoundBox(name):
    '''makeBoundBox(name): makes a BoundBox object.'''
    obj = App.ActiveDocument.addObject("Part::FeaturePython",name)
    _BoundBox(obj)
    if FreeCAD.GuiUp:        
        _ViewProviderBoundBox(obj.ViewObject)
    return obj

class _BoundBox:
    "The BoundBox object"
    def __init__(self,obj):
        self.Type = "BoundBox"
        obj.addProperty("App::PropertyLink","ShapeLink","BoundBox","Object to make a bounding box for")
        
        obj.addProperty("App::PropertyEnumeration","CompoundTraversal","BoundBox","Choose whether to make boundboxes for each child, or use compound overall/")
        obj.CompoundTraversal = ["Use as a whole","Direct children only","Recursive"]
        obj.CompoundTraversal = "Use as a whole"

        obj.addProperty("App::PropertyBool","Precision","BoundBox","Use precise alorithm (slower).")
        obj.Precision = True
        
        obj.addProperty("App::PropertyEnumeration","OrientMode","BoundBox","Choose the orientation of bounding boxes to be made.")
        obj.OrientMode = ["global","local of compound","local of child","use OrientLink"]
        
        obj.addProperty("App::PropertyLink","OrientLink","BoundBox","Link to placement/array to take orientations for bounding boxes")
        
        obj.addProperty("App::PropertyFloat","ScaleFactor","BoundBox","After constructing the bounding box, enlarge/shrink it with respect to center by the scale factor.")
        obj.ScaleFactor = 1.0
        
        obj.addProperty("App::PropertyDistance","Padding","BoundBox","After constructing the bounding box, enlarge/shrink it by specified amount (use negative for shrinking).")
        
        # read-only properties:
        prop = "Size"
        obj.addProperty("App::PropertyVector",prop,"Info","Diagonal vector of bounding box")
        obj.setEditorMode(prop, 1) # set read-only
        
        prop = "Center"
        obj.addProperty("App::PropertyVector",prop,"Info","Center of bounding box")
        obj.setEditorMode(prop, 1) # set read-only
        
        obj.Proxy = self
        

    def execute(self,obj):
        base = screen(obj.ShapeLink).Shape
        if obj.CompoundTraversal == "Use as a whole":
            baseChildren = [base]
        else:
            if base.ShapeType != 'Compound':
                base = Part.makeCompound([base])
            if obj.CompoundTraversal == "Recursive":
                baseChildren = LCE.AllLeaves(base)
            else:
                baseChildren = base.childShapes()
        
        N = len(baseChildren)
        
        orients = []
        if obj.OrientMode == "global":
            orients = [App.Placement()]*N
        elif obj.OrientMode == "local of compound":
            orients = [screen(obj.ShapeLink).Placement]*N
        elif obj.OrientMode == "local of child":
            orients = [child.Placement for child in baseChildren]
        elif obj.OrientMode == "use OrientLink":
            orients = LBF.getPlacementsList(screen(obj.OrientLink), context= obj)
            if len(orients) == N:
                pass
            elif len(orients)>N:
                Executer.warning(obj, "Array of placements linked in OrientLink has more placements ("+str(len(orients))+") than bounding boxes to be constructed ("+str(len(baseChildren))+"). Extra placements will be dropped.")
            elif len(orients)==1:
                orients = [orients[0]]*N
            else:
                raise ValueError(obj.Name+": Array of placements linked in OrientLink has not enough placements ("+str(len(orients))+") than bounding boxes to be constructed ("+str(len(baseChildren))+").")
        else:
            raise ValueError(obj.Name+": OrientMode "+obj.OrientMode+" not implemented =(")
        
        # mark placements with no rotation
        for i in range(N):
            Q = orients[i].Rotation.Q
            # Quaternions for zero rotation are either (0,0,0,1) or (0,0,0,-1). For non-zero
            # rotations, some of first three values will be nonzero, and fourth value will 
            # not be equal to 1. While it's enough to compare absolute value of fourth value
            # to 1, precision is seriously lost in such comparison, so we are checking if 
            # fisrt three values are zero instead.
            if abs(Q[0])+abs(Q[1])+abs(Q[2]) < ParaConfusion:
                orients[i] = None
        
        from lattice2ShapeCopy import shallowCopy
        boxes_shapes = []
        for i in range(N):
            child = baseChildren[i]
            if orients[i] is not None:
                child = shallowCopy(child)
                child.Placement = orients[i].inverse().multiply(child.Placement)

            if obj.Precision:
                bb = getPrecisionBoundBox(child)
            else:
                bb = child.BoundBox
                
            bb = scaledBoundBox(bb, obj.ScaleFactor)
            bb.enlarge(obj.Padding)

            bb_shape = boundBox2RealBox(bb)
            if orients[i] is not None:
                bb_shape.transformShape(orients[i].toMatrix(), True)
            boxes_shapes.append(bb_shape)
            
        #Fill in read-only properties
        if N == 1:
            obj.Size = App.Vector(bb.XLength,bb.YLength,bb.ZLength)
        
            cnt = bb.Center
            if orients[0] is not None:
                cnt = orients[0].multVec(cnt)
            obj.Center = cnt
        else:
            obj.Size = App.Vector()
            obj.Center = App.Vector()
        
        if obj.CompoundTraversal == "Use as a whole":
            assert(N==1)
            obj.Shape = boxes_shapes[0]
        else:
            obj.Shape = Part.makeCompound(boxes_shapes)

    def __getstate__(self):
        return None

    def __setstate__(self,state):
        return None
        
        
class _ViewProviderBoundBox:
    "A View Provider for the BoundBox object"

    def __init__(self,vobj):
        vobj.Proxy = self
        vobj.DisplayMode = "Wireframe"
       
    def getIcon(self):
        if self.Object.CompoundTraversal == "Use as a whole":
            return getIconPath("Lattice2_BoundBox.svg")
        else:
            return getIconPath("Lattice2_BoundBox_Compound.svg")

    def attach(self, vobj):
        self.ViewObject = vobj
        self.Object = vobj.Object

    def __getstate__(self):
        return None

    def __setstate__(self,state):
        return None

def CreateBoundBox(ShapeLink, 
                   CompoundTraversal = "Use as a whole", 
                   Precision = True, 
                   OrientMode = "global",
                   OrientLink = None):
    
    FreeCADGui.addModule("lattice2BoundBox")
    FreeCADGui.addModule("lattice2Executer")

    FreeCADGui.doCommand("f = lattice2BoundBox.makeBoundBox(name= 'BoundBox')")

    FreeCADGui.doCommand("f.ShapeLink = App.ActiveDocument."+ShapeLink.Name)
    if CompoundTraversal == "Use as a whole":
        Label = u"BoundBox of "+ShapeLink.Label
    else:
        Label = u"BoundBoxes of "+ShapeLink.Label
    FreeCADGui.doCommand("f.Label = "+repr(Label))    
    FreeCADGui.doCommand("f.CompoundTraversal = "+repr(CompoundTraversal))    
    FreeCADGui.doCommand("f.Precision = "+repr(Precision))    
    FreeCADGui.doCommand("f.OrientMode = "+repr(OrientMode))    
    if OrientLink is not None:
        FreeCADGui.doCommand("f.OrientLink = App.ActiveDocument."+OrientLink.Name)

    FreeCADGui.doCommand("lattice2Executer.executeFeature(f)")
    FreeCADGui.doCommand("Gui.Selection.addSelection(f)")
    FreeCADGui.doCommand("f = None")

def cmdSingleBoundBox():
    sel = FreeCADGui.Selection.getSelectionEx()
    (lattices, shapes) = LBF.splitSelection(sel)
    if len(shapes) > 0 and len(lattices) == 0:
        FreeCAD.ActiveDocument.openTransaction("Make BoundBox")
        for shape in shapes:
            CreateBoundBox(shape.Object)
        FreeCAD.ActiveDocument.commitTransaction()
    elif len(shapes) == 1 and len(lattices) == 1:
        FreeCAD.ActiveDocument.openTransaction("Make BoundBox")
        CreateBoundBox(shapes[0].Object, OrientMode= "use OrientLink", OrientLink= lattices[0].Object)
        FreeCAD.ActiveDocument.commitTransaction()
    else:
        raise SelectionError("Bad selection", 
                             "Please select some shapes to make bounding boxes of, or a shape and a placement to make a rotated bounding box. You have selected {shapescount} objects and {latticescount} placements/arrays."
                                 .format(  shapescount= str(len(shapes)), 
                                           latticescount= str(len(lattices))  )
                            )
    deselect(sel)

def cmdMultiBoundBox():
    sel = FreeCADGui.Selection.getSelectionEx()
    (lattices, shapes) = LBF.splitSelection(sel)
    if len(shapes) > 0 and len(lattices) == 0:
        FreeCAD.ActiveDocument.openTransaction("Make BoundBox")
        for shape in shapes:
            CreateBoundBox(shape.Object, CompoundTraversal= "Direct children only")
        FreeCAD.ActiveDocument.commitTransaction()
    elif len(shapes) == 1 and len(lattices) == 1:
        FreeCAD.ActiveDocument.openTransaction("Make BoundBox")
        CreateBoundBox(shapes[0].Object, 
                       CompoundTraversal= "Direct children only",
                       OrientMode= "use OrientLink", 
                       OrientLink= lattices[0].Object)
        FreeCAD.ActiveDocument.commitTransaction()
    else:
        raise SelectionError("Bad selection", 
                             "Please select some shapes to make bounding boxes of, or a shape and a placement to make a rotated bounding box. You have selected {shapescount} objects and {latticescount} placements/arrays."
                                 .format(  shapescount= str(len(shapes)), 
                                           latticescount= str(len(lattices))  )
                            )
    deselect(sel)


# -------------------------- /common stuff --------------------------------------------------

# -------------------------- Gui command --------------------------------------------------

class _CommandBoundBoxSingle:
    "Command to create BoundBox feature"
    def GetResources(self):
        return {'Pixmap'  : getIconPath("Lattice2_BoundBox.svg"),
                'MenuText': QtCore.QT_TRANSLATE_NOOP("Lattice2_BoundBox","Bounding Box: whole"),
                'Accel': "",
                'ToolTip': QtCore.QT_TRANSLATE_NOOP("Lattice2_BoundBox","Bounding Box: whole: make a box that precisely fits the whole object")}
        
    def Activated(self):
        try:
            if len(FreeCADGui.Selection.getSelection())==0:
                infoMessage("Bounding Box",
                    "'Bounding Box: whole' command. Makes a box that precisely fits the whole object.\n\n"+
                    "Please select an object, then invoke the command. If you also preselect a placement, the bounding box will be computed in local space of that placement.")
                return
            cmdSingleBoundBox()
        except Exception as err:
            msgError(err)
            
    def IsActive(self):
        if App.ActiveDocument:
            return True
        else:
            return False
            
if FreeCAD.GuiUp:
    FreeCADGui.addCommand('Lattice2_BoundBox_Single', _CommandBoundBoxSingle())

class _CommandBoundBoxMulti:
    "Command to create BoundBox feature"
    def GetResources(self):
        return {'Pixmap'  : getIconPath("Lattice2_BoundBox_Compound.svg"),
                'MenuText': QtCore.QT_TRANSLATE_NOOP("Lattice2_BoundBox","Bounding Box: children"),
                'Accel': "",
                'ToolTip': QtCore.QT_TRANSLATE_NOOP("Lattice2_BoundBox","Bounding Box: children: make bounding boxes around each child of a compound.")}
        
    def Activated(self):
        try:
            if len(FreeCADGui.Selection.getSelection())==0:
                infoMessage("Bounding boxes of children",
                    "'Bounding Box: children' command. Makes a box around each child of a compound.\n\n"+
                    "Please select a compound, then invoke the command. If you also preselect a placement/array of placements, bounding boxes will be constructed in local spaces of corresponding placements.")
                return
            cmdMultiBoundBox()
        except Exception as err:
            msgError(err)
            
    def IsActive(self):
        if App.ActiveDocument:
            return True
        else:
            return False
            
if FreeCAD.GuiUp:
    FreeCADGui.addCommand('Lattice2_BoundBox_Compound', _CommandBoundBoxMulti())

class _CommandBoundBoxGroup:
    def GetCommands(self):
        return ("Lattice2_BoundBox_Single","Lattice2_BoundBox_Compound") 

    def GetDefaultCommand(self): # return the index of the tuple of the default command. 
        return 0

    def GetResources(self):
        return { 'MenuText': 'Bounding Box:', 
                 'ToolTip': 'Bounding Box: make a box that precisely fits a shape.'}
        
    def IsActive(self): # optional
        return activeBody() is None

if FreeCAD.GuiUp:
    FreeCADGui.addCommand('Lattice2_BoundBoxGroupCommand',_CommandBoundBoxGroup())


exportedCommands = ['Lattice2_BoundBoxGroupCommand']

# -------------------------- /Gui command --------------------------------------------------