From 66d7fbd4fa29a5d3950f8bf4ada2105aca17c51c Mon Sep 17 00:00:00 2001 From: "Zheng, Lei" Date: Tue, 3 Mar 2020 09:42:24 +0800 Subject: [PATCH] assembly: add 'Offset' and 'Flip' menu action for AsmElement(Link) --- freecad/asm3/assembly.py | 124 +++++++++++++++++++++++++++++++++---- freecad/asm3/constraint.py | 5 ++ freecad/asm3/mover.py | 96 ++++++++++++++++++++++------ 3 files changed, 194 insertions(+), 31 deletions(-) diff --git a/freecad/asm3/assembly.py b/freecad/asm3/assembly.py index d00775d..b5648fa 100644 --- a/freecad/asm3/assembly.py +++ b/freecad/asm3/assembly.py @@ -691,8 +691,7 @@ class AsmElement(AsmBase): info = None try: - info = getElementInfo(self.getAssembly().getPartGroup(), - self.getElementSubname()) + info = self.getInfo(False) except Exception: self.updatePlacement() @@ -700,8 +699,7 @@ class AsmElement(AsmBase): raise self.fix() - info = getElementInfo(self.getAssembly().getPartGroup(), - self.getElementSubname()) + info = self.getInfo(False) if not getattr(obj,'Radius',None): shape = Part.Shape(info.Shape).copy() @@ -754,8 +752,7 @@ class AsmElement(AsmBase): # Call getElementInfo() to obtain part's placement only. We don't # need the shape here, in order to handle missing down-stream # element - info = getElementInfo(self.getAssembly().getPartGroup(), - self.getElementSubname(),False,True) + info = self.getInfo() pla = info.Placement if obj.Offset.isIdentity(): @@ -1115,6 +1112,10 @@ class AsmElement(AsmBase): subname = '' 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): _iconName = 'Assembly_Assembly_Element.svg' @@ -1159,7 +1160,7 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop): def doubleClicked(self,_vobj): from . import mover - return mover.movePart() + return mover.movePart(element=self.ViewObject.Object, moveElement=False) def getIcon(self): return utils.getIcon(self.__class__, @@ -1248,20 +1249,56 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop): if prop == 'ShowCS': self.setupAxis() - def setupContextMenu(self,vobj,menu): + @staticmethod + def setupMenu(menu, vobj, vobj2): obj = vobj.Object action = QtGui.QAction(QtGui.QIcon(), "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( - action,QtCore.SIGNAL("triggered()"),self.toggleDetach) + action,QtCore.SIGNAL("triggered()"),vobj.Proxy.toggleDetach) menu.addAction(action) if obj.Proxy.isBroken(): action = QtGui.QAction(QtGui.QIcon(), "Fix", menu) - QtCore.QObject.connect( - action,QtCore.SIGNAL("triggered()"),self.fix) + action.setToolTip('Auto fix broken element') + QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),vobj.Proxy.fix) 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): obj = self.ViewObject.Object FreeCAD.setActiveTransaction('Fix element') @@ -1283,6 +1320,39 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop): FreeCAD.closeActiveTransaction(True) 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): def __init__(self,obj,parent): @@ -1950,7 +2020,7 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop): def doubleClicked(self,_vobj): from . import mover - return mover.movePart() + return mover.movePart(element=self.ViewObject.Object, moveElement=False) def canDropObjectEx(self,_obj,owner,subname,elements): if len(elements)>1 or not owner: @@ -1970,6 +2040,32 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop): subname += elements[0] 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): @@ -4196,7 +4292,9 @@ class ViewProviderAssembly(ViewProviderAsmGroup): self.__class__._Busy = False def unsetEdit(self,_vobj,_mode): - self._movingPart = None + if self._movingPart: + self._movingPart.end() + self._movingPart = None return False def showParts(self): diff --git a/freecad/asm3/constraint.py b/freecad/asm3/constraint.py index 3f9c6e1..2d4b190 100644 --- a/freecad/asm3/constraint.py +++ b/freecad/asm3/constraint.py @@ -709,6 +709,11 @@ class Constraint(ProxyType): else: 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): return PropertyInfo(Constraint,name,tp,doc,getter=getter,duplicate=True, diff --git a/freecad/asm3/mover.py b/freecad/asm3/mover.py index 96b6ffd..e43c827 100644 --- a/freecad/asm3/mover.py +++ b/freecad/asm3/mover.py @@ -12,20 +12,72 @@ MovingPartInfo = namedtuple('MovingPartInfo', ('Hierarchy','HierarchyList','ElementInfo','SelObj','SelSubname')) 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.assembly = resolveAssembly(info.Parent) self.viewObject = self.assembly.Object.ViewObject self.info = info + self.element = element 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 = fixed.get(info.Part,None) self.fixedTransform = fixed - if fixed and fixed.Shape: - shape = fixed.Shape - else: - shape = info.Shape + if not shape: + if fixed and fixed.Shape: + shape = fixed.Shape + else: + shape = info.Shape rot = utils.getElementRotation(shape) if not rot: @@ -56,8 +108,6 @@ class AsmMovingPart(object): self.offset = pla.copy() self.offsetInv = pla.inverse() self.draggerPlacement = info.Placement.multiply(pla) - self.trace = None - self.tracePoint = None @classmethod def onRollback(cls): @@ -74,10 +124,17 @@ class AsmMovingPart(object): def begin(self): self.tracePoint = self.TracePosition + def end(self): + for obj,sub in self.sels: + self.view.removeObjectOnTop(obj,sub) + def update(self): info = getElementInfo(self.info.Parent,self.info.SubnameRef) 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) rot = utils.getElementRotation(info.Shape) pla = info.Placement.multiply(FreeCAD.Placement(pos,rot)) @@ -105,11 +162,15 @@ class AsmMovingPart(object): info = self.info part = info.Part obj = self.assembly.Object - pla = self.viewObject.DraggingPlacement + pla = utils.roundPlacement(self.viewObject.DraggingPlacement) updatePla = True 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 if info.Subname.startswith('Vertex'): idx = utils.draftWireVertex2PointIndex(part,info.Subname) @@ -133,8 +194,7 @@ class AsmMovingPart(object): points[idx] = movement.multVec(pt) part.Points = points - elif info.Subname.startswith('Vertex') and \ - utils.isDraftCircle(part): + elif info.Subname.startswith('Vertex') and utils.isDraftCircle(part): updatePla = False a1 = part.FirstAngle a2 = part.LastAngle @@ -226,8 +286,8 @@ def getMovingElementInfo(): '''Extract information from current selection for part moving 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 the highest level assembly in selected hierarchy. @@ -256,7 +316,7 @@ def getMovingElementInfo(): if len(sels[0].SubElementNames)==1: 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 # # info = getElementInfo(r.Object,r.Subname, ...) @@ -286,7 +346,7 @@ def getMovingElementInfo(): assembly = ret[-1].Assembly for r in ret2: 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 # # info = getElementInfo(r.Object,r.Subname, ...) @@ -299,7 +359,7 @@ def getMovingElementInfo(): ElementInfo=info) 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: moveInfo = logger.catch( 'exception when moving part', getMovingElementInfo) @@ -310,7 +370,7 @@ def movePart(useCenterballDragger=None,moveInfo=None): if doc: doc.resetEdit() 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: vobj.UseCenterballDragger = useCenterballDragger FreeCADGui.Selection.clearSelection()