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
@ -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()