assembly: add 'Offset' and 'Flip' menu action for AsmElement(Link)

This commit is contained in:
Zheng, Lei 2020-03-03 09:42:24 +08:00
parent f7c032ed5d
commit 66d7fbd4fa
3 changed files with 194 additions and 31 deletions

View File

@ -691,8 +691,7 @@ class AsmElement(AsmBase):
info = None info = None
try: try:
info = getElementInfo(self.getAssembly().getPartGroup(), info = self.getInfo(False)
self.getElementSubname())
except Exception: except Exception:
self.updatePlacement() self.updatePlacement()
@ -700,8 +699,7 @@ class AsmElement(AsmBase):
raise raise
self.fix() self.fix()
info = getElementInfo(self.getAssembly().getPartGroup(), info = self.getInfo(False)
self.getElementSubname())
if not getattr(obj,'Radius',None): if not getattr(obj,'Radius',None):
shape = Part.Shape(info.Shape).copy() shape = Part.Shape(info.Shape).copy()
@ -754,8 +752,7 @@ class AsmElement(AsmBase):
# Call getElementInfo() to obtain part's placement only. We don't # Call getElementInfo() to obtain part's placement only. We don't
# need the shape here, in order to handle missing down-stream # need the shape here, in order to handle missing down-stream
# element # element
info = getElementInfo(self.getAssembly().getPartGroup(), info = self.getInfo()
self.getElementSubname(),False,True)
pla = info.Placement pla = info.Placement
if obj.Offset.isIdentity(): if obj.Offset.isIdentity():
@ -1115,6 +1112,10 @@ class AsmElement(AsmBase):
subname = '' subname = ''
return obj.getSubObject(subname, retType, mat, transform, depth) return obj.getSubObject(subname, retType, mat, transform, depth)
def getInfo(self, noShape=True):
return getElementInfo(self.getAssembly().getPartGroup(),
self.getElementSubname(),False,noShape)
class ViewProviderAsmElement(ViewProviderAsmOnTop): class ViewProviderAsmElement(ViewProviderAsmOnTop):
_iconName = 'Assembly_Assembly_Element.svg' _iconName = 'Assembly_Assembly_Element.svg'
@ -1159,7 +1160,7 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop):
def doubleClicked(self,_vobj): def doubleClicked(self,_vobj):
from . import mover from . import mover
return mover.movePart() return mover.movePart(element=self.ViewObject.Object, moveElement=False)
def getIcon(self): def getIcon(self):
return utils.getIcon(self.__class__, return utils.getIcon(self.__class__,
@ -1248,20 +1249,56 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop):
if prop == 'ShowCS': if prop == 'ShowCS':
self.setupAxis() self.setupAxis()
def setupContextMenu(self,vobj,menu): @staticmethod
def setupMenu(menu, vobj, vobj2):
obj = vobj.Object obj = vobj.Object
action = QtGui.QAction(QtGui.QIcon(), action = QtGui.QAction(QtGui.QIcon(),
"Attach" if obj.Detach else "Detach", menu) "Attach" if obj.Detach else "Detach", menu)
if obj.Detach:
action.setToolTip('Attach this element to its linked geometry,\n'
'so that it will auto update on change.')
else:
action.setToolTip('Detach this element so that it stays the same\n'
'on change of the linked geometry.')
QtCore.QObject.connect( QtCore.QObject.connect(
action,QtCore.SIGNAL("triggered()"),self.toggleDetach) action,QtCore.SIGNAL("triggered()"),vobj.Proxy.toggleDetach)
menu.addAction(action) menu.addAction(action)
if obj.Proxy.isBroken(): if obj.Proxy.isBroken():
action = QtGui.QAction(QtGui.QIcon(), "Fix", menu) action = QtGui.QAction(QtGui.QIcon(), "Fix", menu)
QtCore.QObject.connect( action.setToolTip('Auto fix broken element')
action,QtCore.SIGNAL("triggered()"),self.fix) QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),vobj.Proxy.fix)
menu.addAction(action) menu.addAction(action)
action = QtGui.QAction(QtGui.QIcon(), 'Offset', menu)
action.setToolTip('Activate dragger to offset this element')
menu.addAction(action)
QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),vobj2.Proxy.offset)
def setupContextMenu(self,vobj,menu):
ViewProviderAsmElement.setupMenu(menu, vobj, vobj)
action = QtGui.QAction(QtGui.QIcon(), 'Flip element', menu)
action.setToolTip('Flip this element\' Z normal by rotating 180 degree\n'
'along the X axis (or Y axis by holding the CTRL key).\n\n'
'Note that this is only effective when for elements\n'
'used in "Attachment" constraint. For others, please\n'
'try "Flip part" instead.')
menu.addAction(action)
QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),self.flip)
action = QtGui.QAction(QtGui.QIcon(), 'Flip part', menu)
action.setToolTip('Flip the owner part using this element Z normal as\n'
'reference, which is done by rotating 180 degree along\n'
'the element\'s X axis (or Y axis by holding the CTRL key).\n\n'
'Note that this won\'t work for elements in "Attachment"\n'
'constraint. Please try "Flip element" instead.')
menu.addAction(action)
QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),self.flipPart)
return True
def fix(self): def fix(self):
obj = self.ViewObject.Object obj = self.ViewObject.Object
FreeCAD.setActiveTransaction('Fix element') FreeCAD.setActiveTransaction('Fix element')
@ -1283,6 +1320,39 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop):
FreeCAD.closeActiveTransaction(True) FreeCAD.closeActiveTransaction(True)
raise raise
def offset(self):
from . import mover
return mover.movePart(element=self.ViewObject.Object, moveElement=True)
@staticmethod
def doFlip(obj, info, flipElement=False):
if QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ControlModifier:
rot = FreeCAD.Rotation(FreeCAD.Vector(0,1,0),180)
else:
rot = FreeCAD.Rotation(FreeCAD.Vector(1,0,0),180)
rot = FreeCAD.Placement(FreeCAD.Vector(), rot)
FreeCAD.setActiveTransaction(
'Flip element' if flipElement else 'Flip part')
try:
if flipElement:
obj.Offset = rot.multiply(obj.Offset)
else:
offset = utils.getElementPlacement(obj.getSubObject(''))
offset = offset.inverse().multiply(rot).multiply(offset)
setPlacement(info.Part, info.Placement.multiply(offset))
FreeCAD.closeActiveTransaction()
except Exception:
FreeCAD.closeActiveTransaction(True)
raise
def flip(self):
obj = self.ViewObject.Object
ViewProviderAsmElement.doFlip(obj, obj.Proxy.getInfo())
def flipPart(self):
obj = self.ViewObject.Object
ViewProviderAsmElement.doFlip(obj, obj.Proxy.getInfo(), False)
class AsmElementSketch(AsmElement): class AsmElementSketch(AsmElement):
def __init__(self,obj,parent): def __init__(self,obj,parent):
@ -1950,7 +2020,7 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop):
def doubleClicked(self,_vobj): def doubleClicked(self,_vobj):
from . import mover from . import mover
return mover.movePart() return mover.movePart(element=self.ViewObject.Object, moveElement=False)
def canDropObjectEx(self,_obj,owner,subname,elements): def canDropObjectEx(self,_obj,owner,subname,elements):
if len(elements)>1 or not owner: if len(elements)>1 or not owner:
@ -1970,6 +2040,32 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop):
subname += elements[0] subname += elements[0]
vobj.Object.Proxy.setLink(owner,subname) vobj.Object.Proxy.setLink(owner,subname)
def setupContextMenu(self,vobj,menu):
element = vobj.Object.LinkedObject
if not isTypeOf(element, AsmElement):
return;
ViewProviderAsmElement.setupMenu(menu, element.ViewObject, vobj)
action = QtGui.QAction(QtGui.QIcon(), 'Flip', menu)
action.setToolTip('For element link inside an "Attachment" constraint,\n'
'flip the element\'s Z normal by rotating 180 degree along\n'
'its X axis (or Y axis by holding the CTRL key). For other\n'
'constraint, flip the owner part instead.')
menu.addAction(action)
QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),self.flip)
return True
def offset(self):
from . import mover
return mover.movePart(element=self.ViewObject.Object, moveElement=True)
def flip(self):
obj = self.ViewObject.Object
ViewProviderAsmElement.doFlip(obj, obj.Proxy.getInfo(),
Constraint.isAttachment(obj.Proxy.parent.Object))
class AsmConstraint(AsmGroup): class AsmConstraint(AsmGroup):
@ -4196,7 +4292,9 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
self.__class__._Busy = False self.__class__._Busy = False
def unsetEdit(self,_vobj,_mode): def unsetEdit(self,_vobj,_mode):
self._movingPart = None if self._movingPart:
self._movingPart.end()
self._movingPart = None
return False return False
def showParts(self): def showParts(self):

View File

@ -709,6 +709,11 @@ class Constraint(ProxyType):
else: else:
obj.setExpression('.Label2', None) obj.setExpression('.Label2', None)
@classmethod
def isAttachment(mcs, obj):
cstr = mcs.getProxy(obj)
return isinstance(cstr, Attachment)
def _makeProp(name,tp,doc='',getter=propGet,internal=False,default=None): def _makeProp(name,tp,doc='',getter=propGet,internal=False,default=None):
return PropertyInfo(Constraint,name,tp,doc,getter=getter,duplicate=True, return PropertyInfo(Constraint,name,tp,doc,getter=getter,duplicate=True,

View File

@ -12,20 +12,72 @@ MovingPartInfo = namedtuple('MovingPartInfo',
('Hierarchy','HierarchyList','ElementInfo','SelObj','SelSubname')) ('Hierarchy','HierarchyList','ElementInfo','SelObj','SelSubname'))
class AsmMovingPart(object): class AsmMovingPart(object):
def __init__(self,hierarchy,info): def __init__(self, moveInfo, element, moveElement):
hierarchy = moveInfo.HierarchyList
info = moveInfo.ElementInfo
self.objs = [h.Assembly for h in reversed(hierarchy)] self.objs = [h.Assembly for h in reversed(hierarchy)]
self.assembly = resolveAssembly(info.Parent) self.assembly = resolveAssembly(info.Parent)
self.viewObject = self.assembly.Object.ViewObject self.viewObject = self.assembly.Object.ViewObject
self.info = info self.info = info
self.element = element
self.undos = None self.undos = None
self.trace = None
self.tracePoint = None
self.moveElement = moveElement
self.sels = []
view = self.viewObject.Document.ActiveView
shape = None
if hasattr(view, 'addObjectOnTop'):
self.view = view
else:
self.view = None
if element:
if self.view:
self.sels.append((moveInfo.SelObj, moveInfo.SelSubname))
view.addObjectOnTop(*self.sels[0])
logger.debug('group on top {}.{}',
moveInfo.SelObj.Name, moveInfo.SelSubname)
shape = element.getSubObject('')
# whether to move element itself or its owner part
if moveElement:
self.bbox = shape.BoundBox
# Place the dragger at element's current (maybe offseted) shape
# center point in assembly coordinate
self.draggerPlacement = utils.getElementPlacement(shape)
# calculate the placement of an unoffseted element in assembly coordinate
self.offset = utils.getElementPlacement(element.getSubObject('',transform=False))
# Calculate the placement to transform the unoffseted element
# shape to the origin of its own coordinate space
self.offsetInv = self.offset.inverse()
return
# if we are not moving the element, but its owner part, transform
# the element shape to part's coordinate space
shape.Placement = shape.Placement.multiply(info.Placement.inverse());
if self.view:
sub = moveInfo.SelSubname[:-len(info.SubnameRef)]
if isinstance(info.Part,tuple):
sub += '2.{}.{}.'.format(info.Part[0].Name,info.Part[1])
else:
sub += '2.{}.'.format(info.Part.Name)
self.sels.append((moveInfo.SelObj, sub))
logger.debug('group on top {}.{}', moveInfo.SelObj.Name,sub)
view.addObjectOnTop(*self.sels[-1])
fixed = Constraint.getFixedTransform(self.assembly.getConstraints()) fixed = Constraint.getFixedTransform(self.assembly.getConstraints())
fixed = fixed.get(info.Part,None) fixed = fixed.get(info.Part,None)
self.fixedTransform = fixed self.fixedTransform = fixed
if fixed and fixed.Shape: if not shape:
shape = fixed.Shape if fixed and fixed.Shape:
else: shape = fixed.Shape
shape = info.Shape else:
shape = info.Shape
rot = utils.getElementRotation(shape) rot = utils.getElementRotation(shape)
if not rot: if not rot:
@ -56,8 +108,6 @@ class AsmMovingPart(object):
self.offset = pla.copy() self.offset = pla.copy()
self.offsetInv = pla.inverse() self.offsetInv = pla.inverse()
self.draggerPlacement = info.Placement.multiply(pla) self.draggerPlacement = info.Placement.multiply(pla)
self.trace = None
self.tracePoint = None
@classmethod @classmethod
def onRollback(cls): def onRollback(cls):
@ -74,10 +124,17 @@ class AsmMovingPart(object):
def begin(self): def begin(self):
self.tracePoint = self.TracePosition self.tracePoint = self.TracePosition
def end(self):
for obj,sub in self.sels:
self.view.removeObjectOnTop(obj,sub)
def update(self): def update(self):
info = getElementInfo(self.info.Parent,self.info.SubnameRef) info = getElementInfo(self.info.Parent,self.info.SubnameRef)
self.info = info self.info = info
if utils.isDraftObject(info.Part): if self.element:
shape = self.element.getSubObject('')
pla = utils.getElementPlacement(shape)
elif utils.isDraftObject(info.Part):
pos = utils.getElementPos(info.Shape) pos = utils.getElementPos(info.Shape)
rot = utils.getElementRotation(info.Shape) rot = utils.getElementRotation(info.Shape)
pla = info.Placement.multiply(FreeCAD.Placement(pos,rot)) pla = info.Placement.multiply(FreeCAD.Placement(pos,rot))
@ -105,11 +162,15 @@ class AsmMovingPart(object):
info = self.info info = self.info
part = info.Part part = info.Part
obj = self.assembly.Object obj = self.assembly.Object
pla = self.viewObject.DraggingPlacement pla = utils.roundPlacement(self.viewObject.DraggingPlacement)
updatePla = True updatePla = True
rollback = [] rollback = []
if not info.Subname.startswith('Face') and utils.isDraftWire(part): if self.moveElement:
updatePla = False
offset = utils.roundPlacement(self.offsetInv.multiply(pla))
self.element.Offset = offset
elif not info.Subname.startswith('Face') and utils.isDraftWire(part):
updatePla = False updatePla = False
if info.Subname.startswith('Vertex'): if info.Subname.startswith('Vertex'):
idx = utils.draftWireVertex2PointIndex(part,info.Subname) idx = utils.draftWireVertex2PointIndex(part,info.Subname)
@ -133,8 +194,7 @@ class AsmMovingPart(object):
points[idx] = movement.multVec(pt) points[idx] = movement.multVec(pt)
part.Points = points part.Points = points
elif info.Subname.startswith('Vertex') and \ elif info.Subname.startswith('Vertex') and utils.isDraftCircle(part):
utils.isDraftCircle(part):
updatePla = False updatePla = False
a1 = part.FirstAngle a1 = part.FirstAngle
a2 = part.LastAngle a2 = part.LastAngle
@ -226,8 +286,8 @@ def getMovingElementInfo():
'''Extract information from current selection for part moving '''Extract information from current selection for part moving
It returns a tuple containing the selected assembly hierarchy, and It returns a tuple containing the selected assembly hierarchy, and
AsmElementInfo of the selected child part object. AsmElementInfo of the selected child part object.
If there is only one selection, then the moving part will be one belong to If there is only one selection, then the moving part will be one belong to
the highest level assembly in selected hierarchy. the highest level assembly in selected hierarchy.
@ -256,7 +316,7 @@ def getMovingElementInfo():
if len(sels[0].SubElementNames)==1: if len(sels[0].SubElementNames)==1:
r = ret[0] r = ret[0]
# Warning! Must not calling like below, because r.Assembly maybe a link, # Warning! Must not call like below, because r.Assembly maybe a link,
# and we need that information # and we need that information
# #
# info = getElementInfo(r.Object,r.Subname, ...) # info = getElementInfo(r.Object,r.Subname, ...)
@ -286,7 +346,7 @@ def getMovingElementInfo():
assembly = ret[-1].Assembly assembly = ret[-1].Assembly
for r in ret2: for r in ret2:
if assembly == r.Assembly: if assembly == r.Assembly:
# Warning! Must not calling like below, because r.Assembly maybe a # Warning! Must not call like below, because r.Assembly maybe a
# link, and we need that information # link, and we need that information
# #
# info = getElementInfo(r.Object,r.Subname, ...) # info = getElementInfo(r.Object,r.Subname, ...)
@ -299,7 +359,7 @@ def getMovingElementInfo():
ElementInfo=info) ElementInfo=info)
raise RuntimeError('not child parent selection') raise RuntimeError('not child parent selection')
def movePart(useCenterballDragger=None,moveInfo=None): def movePart(useCenterballDragger=None, moveInfo=None, element=None, moveElement=False):
if not moveInfo: if not moveInfo:
moveInfo = logger.catch( moveInfo = logger.catch(
'exception when moving part', getMovingElementInfo) 'exception when moving part', getMovingElementInfo)
@ -310,7 +370,7 @@ def movePart(useCenterballDragger=None,moveInfo=None):
if doc: if doc:
doc.resetEdit() doc.resetEdit()
vobj = resolveAssembly(info.Parent).Object.ViewObject vobj = resolveAssembly(info.Parent).Object.ViewObject
vobj.Proxy._movingPart = AsmMovingPart(moveInfo.HierarchyList,info) vobj.Proxy._movingPart = AsmMovingPart(moveInfo, element, moveElement)
if useCenterballDragger is not None: if useCenterballDragger is not None:
vobj.UseCenterballDragger = useCenterballDragger vobj.UseCenterballDragger = useCenterballDragger
FreeCADGui.Selection.clearSelection() FreeCADGui.Selection.clearSelection()