Lattice2/lattice2BoundBox.py
DeepSOIC 0b591d5189 active body awareness
disable some commands when in body
2018-06-14 01:39:28 +03:00

407 lines
18 KiB
Python

#***************************************************************************
#* *
#* 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 --------------------------------------------------