Refactor part move command

This commit is contained in:
Zheng, Lei 2017-10-30 04:01:33 +08:00
parent 398b1fad86
commit f16fbf52a8
2 changed files with 463 additions and 225 deletions

View File

@ -8,6 +8,8 @@ from asm3.constraint import Constraint
from asm3.system import System from asm3.system import System
def setupUndo(doc,undoDocs,name): def setupUndo(doc,undoDocs,name):
if undoDocs is None:
return
if doc.HasPendingTransaction or doc in undoDocs: if doc.HasPendingTransaction or doc in undoDocs:
return return
if not name: if not name:
@ -110,7 +112,7 @@ class ViewProviderAsmGroup(ViewProviderAsmBase):
def claimChildren(self): def claimChildren(self):
return self.ViewObject.Object.Group return self.ViewObject.Object.Group
def doubleClicked(self): def doubleClicked(self, _vobj):
return False return False
@ -300,11 +302,160 @@ class ViewProviderAsmElement(ViewProviderAsmBase):
vobj.ShapeMaterial.EmissiveColor = self.getDefaultColor() vobj.ShapeMaterial.EmissiveColor = self.getDefaultColor()
vobj.DrawStyle = 1 vobj.DrawStyle = 1
vobj.LineWidth = 4 vobj.LineWidth = 4
vobj.PointSize = 6 vobj.PointSize = 8
def getDefaultColor(self): def getDefaultColor(self):
return (60.0/255.0,1.0,1.0) return (60.0/255.0,1.0,1.0)
PartInfo = namedtuple('AsmPartInfo', ('Parent','SubnameRef','Part',
'PartName','Placement','Object','Subname','Shape'))
def getPartInfo(parent, subname):
'''Return a named tuple containing the part object element information
Parameters:
parent: the parent document object, either an assembly, or a part group
subname: subname reference to the part element (i.e. edge, face, vertex)
Return a named tuple with the following fields:
Parent: set to the input parent object
SubnameRef: set to the input subname reference
Part: either the part object, or a tuple(obj, idx) to refer to an element in
an link array,
PartName: a string name for the part
Placement: the placement of the part
Object: the object that owns the element. In case 'Part' is an assembly, we
the element owner will always be some (grand)child of the 'Part'
Subname: the subname reference to the element owner object. The reference is
realtive to the 'Part', i.e. Object = Part.getSubObject(subname), or if
'Part' is a tuple, Object = Part[0].getSubObject(str(Part[1]) + '.' +
subname)
Shape: Part.Shape of the linked element. The shape's placement is relative
to the owner Part.
'''
subnameRef = subname
names = subname.split('.')
if isTypeOf(parent,Assembly):
child = parent.getSubObject(names[0]+'.',1)
if not child:
raise RuntimeError('Invalid sub object {}, {}'.format(
objName(parent), subname))
if isTypeOf(child,AsmElementGroup):
raise RuntimeError('Element object cannot be moved directly')
if isTypeOf(child,AsmConstraintGroup):
child = parent.getSubObject(subname,1)
if not child:
raise RuntimeError('Invalid sub object {}, {}'.format(
objName(parent), subname))
if not isTypeOf(child,AsmElementLink):
raise RuntimeError('{} cannot be moved'.format(objName(child)))
return child.Proxy.getInfo()
partGroup = child
names = names[1:]
subname = '.'.join(names)
elif isTypeOf(parent,AsmPartGroup):
partGroup = parent
else:
raise RuntimeError('{} is not Assembly or PartGroup'.format(
objName(parent)))
part = partGroup.getSubObject(names[0]+'.',1)
if not part:
raise RuntimeError('Invalid sub object {}, {}'.format(
objName(parent), subnameRef))
# For storing the shape of the element with proper transformation
shape = None
# For storing the placement of the movable part
pla = None
# For storing the actual geometry object of the part, in case 'part' is
# a link
obj = None
if not isTypeOf(part,Assembly,True):
getter = getattr(part.getLinkedObject(True),
'getLinkExtProperty',None)
# special treatment of link array (i.e. when ElementCount!=0), we
# allow the array element to be moveable by the solver
if getter and getter('ElementCount'):
# store both the part (i.e. the link array), and the array
# element object
part = (part,part.getSubObject(names[1]+'.',1))
# trim the subname to be after the array element
sub = '.'.join(names[2:])
# There are two states of an link array.
if getter('ElementList'):
# a) The elements are expanded as individual objects, i.e
# when ElementList has members, then the moveable Placement
# is a property of the array element. So we obtain the shape
# before 'Placement' by setting 'transform' set to False.
shape=part[1].getSubObject(sub,transform=False)
pla = part[1].Placement
obj = part[0].getLinkedObject(False)
partName = part[1].Name
else:
# b) The elements are collapsed. Then the moveable Placement
# is stored inside link object's PlacementList property. So,
# the shape obtained below is already before 'Placement',
# i.e. no need to set 'transform' to False.
shape=part[1].getSubObject(sub)
obj = part[1]
try:
idx = names[1].split('_i')[-1]
# we store the array index instead, in order to modified
# Placement later when the solver is done. Also because
# that when the elements are collapsed, there is really
# no element object here.
part = (part[0],int(idx),part[1])
pla = part[0].PlacementList[idx]
except ValueError:
raise RuntimeError('invalid array subname of element {}: '
'{}'.format(objName(parent),subnameRef))
partName = '{}.{}.'.format(part[0].Name,idx)
subname = sub
if not shape:
# Here means, either the 'part' is an assembly or it is a non array
# object. We trim the subname reference to be relative to the part
# object. And obtain the shape before part's Placement by setting
# 'transform' to False
subname = '.'.join(names[1:])
shape = part.getSubObject(subname,transform=False)
pla = part.Placement
obj = part.getLinkedObject(False)
partName = part.Name
return PartInfo(Parent = parent,
SubnameRef = subnameRef,
Part = part,
PartName = partName,
Placement = pla.copy(),
Object = obj,
Subname = subname,
Shape = shape.copy())
class AsmElementLink(AsmBase): class AsmElementLink(AsmBase):
def __init__(self,parent): def __init__(self,parent):
@ -322,8 +473,8 @@ class AsmElementLink(AsmBase):
obj.addProperty("App::PropertyXLink","LinkedObject"," Link",'') obj.addProperty("App::PropertyXLink","LinkedObject"," Link",'')
super(AsmElementLink,self).attach(obj) super(AsmElementLink,self).attach(obj)
def execute(self,obj): def execute(self,_obj):
obj.ViewObject.Proxy.onExecute(self.getInfo(True)) self.getInfo(True)
return False return False
def getElement(self): def getElement(self):
@ -344,7 +495,16 @@ class AsmElementLink(AsmBase):
objName(self.Object))) objName(self.Object)))
return link[1] return link[1]
def getShapeSubName(self): def getElementSubname(self):
'''Resolve element link subname
AsmElementLink links to elements of a constraint. It always link to an
AsmElement object belonging to the same parent assembly. AsmElement is
also a link, which links to an arbitarily nested part object. This
function is for resolving the AsmElementLink's subname reference to the
actual part object subname reference relative to the parent assembly's
part group
'''
element = self.getElement() element = self.getElement()
assembly = element.getAssembly() assembly = element.getAssembly()
if assembly == self.getAssembly(): if assembly == self.getAssembly():
@ -381,7 +541,7 @@ class AsmElementLink(AsmBase):
return (owner,subname) return (owner,subname)
# try to see if the reference comes from some nested assembly # try to see if the reference comes from some nested assembly
ret = assembly.findChild(owner,subname,recursive=True) ret = assembly.find(owner,subname,recursive=True)
if not ret: if not ret:
# It is from a non assembly child part, then use our own element # It is from a non assembly child part, then use our own element
# group as the holder for elements # group as the holder for elements
@ -416,9 +576,6 @@ class AsmElementLink(AsmBase):
else: else:
obj.Label = obj.Name obj.Label = obj.Name
Info = namedtuple('AsmElementLinkInfo',
('Part','PartName','Placement','Object','Subname','Shape'))
def getInfo(self,refresh=False): def getInfo(self,refresh=False):
if not refresh: if not refresh:
ret = getattr(self,'info',None) ret = getattr(self,'info',None)
@ -427,91 +584,8 @@ class AsmElementLink(AsmBase):
self.info = None self.info = None
if not getattr(self,'Object',None): if not getattr(self,'Object',None):
return return
assembly = self.getAssembly() self.info = getPartInfo(self.getAssembly().getPartGroup(),
subname = self.getShapeSubName() self.getElementSubname())
names = subname.split('.')
partGroup = assembly.getPartGroup()
part = partGroup.getSubObject(names[0]+'.',1)
if not part:
raise RuntimeError('Eelement link "{}" borken: {}'.format(
objName(self.Object),subname))
# For storing the shape of the element with proper transformation
shape = None
# For storing the placement of the movable part
pla = None
# For storing the actual geometry object of the part, in case 'part' is
# a link
obj = None
if not isTypeOf(part,Assembly,True) and \
not Constraint.isDisabled(self.parent.Object) and \
not Constraint.isLocked(self.parent.Object):
getter = getattr(part.getLinkedObject(True),
'getLinkExtProperty',None)
# special treatment of link array (i.e. when ElementCount!=0), we
# allow the array element to be moveable by the solver
if getter and getter('ElementCount'):
# store both the part (i.e. the link array), and the array
# element object
part = (part,part.getSubObject(names[1]+'.',1))
# trim the subname to be after the array element
sub = '.'.join(names[2:])
# There are two states of an link array.
if getter('ElementList'):
# a) The elements are expanded as individual objects, i.e
# when ElementList has members, then the moveable Placement
# is a property of the array element. So we obtain the shape
# before 'Placement' by setting 'transform' set to False.
shape=part[1].getSubObject(sub,transform=False)
pla = part[1].Placement
obj = part[0].getLinkedObject(False)
partName = part[1].Name
else:
# b) The elements are collapsed. Then the moveable Placement
# is stored inside link object's PlacementList property. So,
# the shape obtained below is already before 'Placement',
# i.e. no need to set 'transform' to False.
shape=part[1].getSubObject(sub)
obj = part[1]
try:
idx = names[1].split('_i')[-1]
# we store the array index instead, in order to modified
# Placement later when the solver is done. Also because
# that when the elements are collapsed, there is really
# no element object here.
part = (part[0],int(idx),part[1])
pla = part[0].PlacementList[idx]
except ValueError:
raise RuntimeError('invalid array subname of element '
'{}: {}'.format(objName(self.Object),subname))
partName = '{}.{}.'.format(part[0].Name,idx)
subname = sub
if not shape:
# Here means, either the 'part' is an assembly or it is a non array
# object. We trim the subname reference to be relative to the part
# object. And obtain the shape before part's Placement by setting
# 'transform' to False
subname = '.'.join(names[1:])
shape = part.getSubObject(subname,transform=False)
pla = part.Placement
obj = part.getLinkedObject(False)
partName = part.Name
self.info = AsmElementLink.Info(Part = part,
PartName = partName,
Placement = pla.copy(),
Object = obj,
Subname = subname,
Shape = shape.copy())
return self.info return self.info
@staticmethod @staticmethod
@ -548,84 +622,10 @@ class AsmElementLink(AsmBase):
def setPlacement(part,pla,undoDocs,undoName=None): def setPlacement(part,pla,undoDocs,undoName=None):
AsmElementLink.setPlacement(part,pla,undoDocs,undoName) AsmElementLink.setPlacement(part,pla,undoDocs,undoName)
class AsmDraggingContext(object):
def __init__(self,info):
self.undos = None
self.part = info.Part
rot = utils.getElementRotation(info.Shape)
if not rot:
# in case the shape has no normal, like a vertex, just use an empty
# rotation, which means having the same rotation has the owner part.
rot = FreeCAD.Rotation()
pla = FreeCAD.Placement(utils.getElementPos(info.Shape),rot)
self.offset = FreeCAD.Placement(pla.toMatrix())
self.offsetInv = FreeCAD.Placement(pla.toMatrix().inverse())
self.placement = info.Placement.multiply(pla)
self.tracePoint = self.placement.Base
self.trace = None
def update(self,info):
self.part = info.Part
pla = info.Placement.multiply(FreeCAD.Placement(self.offset))
self.placement = pla
if asm3.gui.AsmCmdManager.Trace and \
self.tracePoint.isEqual(pla.Base,1e5):
if not self.trace:
self.trace = FreeCAD.ActiveDocument.addObject(
'Part::Polygon','AsmTrace')
self.trace.Nodes = {-1:self.tracePoint}
self.tracePoint = pla.Base
self.trace.Nodes = {-1:pla.Base}
self.trace.recompute()
return pla
class ViewProviderAsmElementLink(ViewProviderAsmBase): class ViewProviderAsmElementLink(ViewProviderAsmBase):
def __init__(self,vobj): def doubleClicked(self,_vobj):
self._draggingContext = None return movePart()
super(ViewProviderAsmElementLink,self).__init__(vobj)
def doubleClicked(self, vobj):
return vobj.Document.setEdit(vobj,1)
def onExecute(self,info):
if not getattr(self,'_draggingContext',None):
return
self.ViewObject.DraggingPlacement = self._draggingContext.update(info)
def initDraggingPlacement(self):
info = self.ViewObject.Object.Proxy.getInfo()
self._draggingContext = AsmDraggingContext(info)
return (FreeCADGui.editDocument().EditingTransform,
self._draggingContext.placement,
info.Shape.BoundBox)
def onDragStart(self):
self._draggingContext.undos = set()
def onDragMotion(self):
ctx = self._draggingContext
pla = self.ViewObject.DraggingPlacement.multiply(ctx.offsetInv)
setPlacement(ctx.part,pla,ctx.undos, 'Assembly drag')
from PySide import QtCore,QtGui
obj = self.ViewObject.Object
if QtGui.QApplication.keyboardModifiers() == QtCore.Qt.ControlModifier:
obj.getLinkedObject(False).recompute()
obj.recompute()
return
obj.Proxy.parent.solve()
return ctx.placement
def onDragEnd(self):
for doc in self._draggingContext.undos:
doc.commitTransaction()
def unsetEdit(self,_vobj,_mode):
self._draggingContext = None
return False
class AsmConstraint(AsmGroup): class AsmConstraint(AsmGroup):
@ -636,14 +636,6 @@ class AsmConstraint(AsmGroup):
self.parent = getProxy(parent,AsmConstraintGroup) self.parent = getProxy(parent,AsmConstraintGroup)
super(AsmConstraint,self).__init__() super(AsmConstraint,self).__init__()
def solve(self, excp=False):
try:
asm3.solver.solve(self.getAssembly().Object)
except RuntimeError as e:
if excp:
raise e
logger.error(e)
def getAssembly(self): def getAssembly(self):
return self.parent.parent return self.parent.parent
@ -653,8 +645,7 @@ class AsmConstraint(AsmGroup):
obj = getattr(self,'Object',None) obj = getattr(self,'Object',None)
if not obj: if not obj:
return return
if Constraint.isLocked(obj) or \ if Constraint.isDisabled(obj):
Constraint.isDisabled(obj):
return return
parent = getattr(self,'parent',None) parent = getattr(self,'parent',None)
if not parent: if not parent:
@ -726,7 +717,7 @@ class AsmConstraint(AsmGroup):
''' '''
sels = FreeCADGui.Selection.getSelectionEx('',False) sels = FreeCADGui.Selection.getSelectionEx('',False)
if not sels: if not sels:
return raise RuntimeError('no selection')
if len(sels)>1: if len(sels)>1:
raise RuntimeError( raise RuntimeError(
'The selections must have a common (grand)parent assembly') 'The selections must have a common (grand)parent assembly')
@ -737,7 +728,10 @@ class AsmConstraint(AsmGroup):
assembly = None assembly = None
for sub in sel.SubElementNames: for sub in sel.SubElementNames:
sobj = sel.Object.getSubObject(sub,1) sobj = sel.Object.getSubObject(sub,1)
ret = Assembly.findChild(sel.Object,sub,recursive=True) if not sobj:
raise RuntimeError('Cannot find sub-object "{}" of {}'.format(
sub,sel.Object))
ret = Assembly.find(sel.Object,sub,recursive=True)
if not ret: if not ret:
raise RuntimeError('Selection {}.{} is not from an ' raise RuntimeError('Selection {}.{} is not from an '
'assembly'.format(sel.Object.Name,sub)) 'assembly'.format(sel.Object.Name,sub))
@ -768,7 +762,7 @@ class AsmConstraint(AsmGroup):
typeid = Constraint.getTypeID(cstr) typeid = Constraint.getTypeID(cstr)
info = cstr.Proxy.getInfo() info = cstr.Proxy.getInfo()
check = [o.getShape() for o in info.Elements] + elements check = [o.getShape() for o in info.Elements] + elements
elif typeid: else:
check = elements check = elements
if check: if check:
Constraint.check(typeid,check) Constraint.check(typeid,check)
@ -778,25 +772,39 @@ class AsmConstraint(AsmGroup):
Elements = elements) Elements = elements)
@staticmethod @staticmethod
def make(typeid, selection=None, name='Constraint'): def make(typeid, selection=None, name='Constraint', undo=True):
if not selection: if not selection:
selection = AsmConstraint.getSelection(typeid) selection = AsmConstraint.getSelection(typeid)
if selection.Constraint: if selection.Constraint:
cstr = selection.Constraint cstr = selection.Constraint
if undo:
doc = cstr.Document
doc.openTransaction('Assembly change constraint')
else: else:
constraints = selection.Assembly.Proxy.getConstraintGroup() constraints = selection.Assembly.Proxy.getConstraintGroup()
if undo:
doc = constraints.Document
doc.openTransaction('Assembly make constraint')
cstr = constraints.Document.addObject("App::FeaturePython", cstr = constraints.Document.addObject("App::FeaturePython",
name,AsmConstraint(constraints),None,True) name,AsmConstraint(constraints),None,True)
ViewProviderAsmConstraint(cstr.ViewObject) ViewProviderAsmConstraint(cstr.ViewObject)
constraints.setLink({-1:cstr}) constraints.setLink({-1:cstr})
Constraint.setTypeID(cstr,typeid) Constraint.setTypeID(cstr,typeid)
for e in selection.Elements: try:
AsmElementLink.make(AsmElementLink.MakeInfo(cstr,*e)) for e in selection.Elements:
cstr.Proxy._initializing = False AsmElementLink.make(AsmElementLink.MakeInfo(cstr,*e))
if cstr.recompute() and asm3.gui.AsmCmdManager.AutoRecompute: cstr.Proxy._initializing = False
cstr.Proxy.solve() if cstr.recompute() and asm3.gui.AsmCmdManager.AutoRecompute:
return cstr logger.catch('solver exception when auto recompute',
asm3.solver.solve, selection.Assembly.Object, undo=undo)
if undo:
doc.commitTransaction()
return cstr
except Exception:
if undo:
doc.abortTransaction()
raise
class ViewProviderAsmConstraint(ViewProviderAsmGroup): class ViewProviderAsmConstraint(ViewProviderAsmGroup):
@ -902,6 +910,7 @@ class Assembly(AsmGroup):
self.constraints = None self.constraints = None
self.buildShape() self.buildShape()
System.touch(obj) System.touch(obj)
obj.ViewObject.Proxy.onExecute()
return False # return False to call LinkBaseExtension::execute() return False # return False to call LinkBaseExtension::execute()
def onSolverChanged(self,setup=False): def onSolverChanged(self,setup=False):
@ -1016,6 +1025,11 @@ class Assembly(AsmGroup):
logger.debug('skip constraint "{}" type ' logger.debug('skip constraint "{}" type '
'{}'.format(objName(o),o.Type)) '{}'.format(objName(o),o.Type))
continue continue
if not System.isConstraintSupported(self.Object,
Constraint.getTypeName(o)):
logger.debug('skip unsupported constraint "{}" type '
'{}'.format(objName(o),o.Type))
continue
ret.append(o) ret.append(o)
self.constraints = ret self.constraints = ret
return self.constraints return self.constraints
@ -1078,7 +1092,7 @@ class Assembly(AsmGroup):
Info = namedtuple('AssemblyInfo',('Assembly','Object','Subname')) Info = namedtuple('AssemblyInfo',('Assembly','Object','Subname'))
@staticmethod @staticmethod
def find(sels=None): def getSelection(sels=None):
'Find all assembly objects among the current selection' 'Find all assembly objects among the current selection'
objs = set() objs = set()
if sels is None: if sels is None:
@ -1089,14 +1103,14 @@ class Assembly(AsmGroup):
objs.add(sel.Object) objs.add(sel.Object)
continue continue
for subname in sel.SubElementNames: for subname in sel.SubElementNames:
ret = Assembly.findChild(sel.Object,subname,recursive=True) ret = Assembly.find(sel.Object,subname,recursive=True)
if ret: if ret:
objs.add(ret[-1].Assembly) objs.add(ret[-1].Assembly)
return tuple(objs) return tuple(objs)
@staticmethod @staticmethod
def findChild(obj,subname,childType=None, def find(obj,subname,childType=None,
recursive=False,relativeToChild=True): recursive=False,relativeToChild=True,keepEmptyChild=False):
''' '''
Find the immediate child of the first Assembly referenced in 'subs' Find the immediate child of the first Assembly referenced in 'subs'
@ -1119,55 +1133,256 @@ class Assembly(AsmGroup):
''' '''
assembly = None assembly = None
child = None child = None
idx = -1
if isTypeOf(obj,Assembly,True): if isTypeOf(obj,Assembly,True):
assembly = obj assembly = obj
subs = subname if isinstance(subname,list) else subname.split('.') subs = subname if isinstance(subname,list) else subname.split('.')
i= 0
for i,name in enumerate(subs[:-1]): for i,name in enumerate(subs[:-1]):
obj = obj.getSubObject(name+'.',1) sobj = obj.getSubObject(name+'.',1)
if not obj: if not sobj:
raise RuntimeError('Cannot find sub object {}'.format(name)) raise RuntimeError('Cannot find sub object {}, '
'{}'.format(objName(obj),name))
obj = sobj
if assembly and isTypeOf(obj,childType): if assembly and isTypeOf(obj,childType):
child = obj child = obj
if relativeToChild:
idx = i+1
else:
idx = i
break break
assembly = obj if isTypeOf(obj,Assembly,True) else None assembly = obj if isTypeOf(obj,Assembly,True) else None
if not child: if not child:
if keepEmptyChild and assembly:
ret = Assembly.Info(Assembly=assembly,Object=None,Subname='')
return [ret] if recursive else ret
return return
subs = subs[idx:] ret = Assembly.Info(Assembly = assembly, Object = child,
ret = Assembly.Info(Assembly = assembly, Subname = '.'.join(subs[i+1:] if relativeToChild else subs[i:]))
Object = child,
Subname = '.'.join(subs))
if not recursive: if not recursive:
return ret return ret
nret = Assembly.findChild(child,subs,childType,True) nret = Assembly.find(child, subs[i+1:], childType, recursive,
relativeToChild, keepEmptyChild)
if nret: if nret:
return [ret] + nret return [ret] + nret
return [ret] return [ret]
@staticmethod
def findChildren(obj,subname,tp=None):
return Assembly.find(obj,subname,tp,True,False,True)
@staticmethod @staticmethod
def findPartGroup(obj,subname='2.',recursive=False,relativeToChild=True): def findPartGroup(obj,subname='2.',recursive=False,relativeToChild=True):
return Assembly.findChild( return Assembly.find(
obj,subname,AsmPartGroup,recursive,relativeToChild) obj,subname,AsmPartGroup,recursive,relativeToChild)
@staticmethod @staticmethod
def findElementGroup(obj,subname='1.',relativeToChild=True): def findElementGroup(obj,subname='1.',relativeToChild=True):
return Assembly.findChild( return Assembly.find(
obj,subname,AsmElementGroup,False,relativeToChild) obj,subname,AsmElementGroup,False,relativeToChild)
@staticmethod @staticmethod
def findConstraintGroup(obj,subname='0.',relativeToChild=True): def findConstraintGroup(obj,subname='0.',relativeToChild=True):
return Assembly.findChild( return Assembly.find(
obj,subname,AsmConstraintGroup,False,relativeToChild) obj,subname,AsmConstraintGroup,False,relativeToChild)
class AsmMovingPart(object):
def __init__(self,hierarchy,info):
self.objs = [h.Assembly for h in reversed(hierarchy)]
if isTypeOf(info.Parent,Assembly):
self.assembly = info.Parent.Proxy
elif isTypeOf(info.Parent,AsmPartGroup):
self.assembly = info.Parent.Proxy.parent
else:
raise RuntimeError('invalid moving part parent object {}'.format(
objName(info.Parent)))
self.parent = info.Parent
self.subname = info.SubnameRef
self.undos = None
self.part = info.Part
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
rot = utils.getElementRotation(shape)
if not rot:
# in case the shape has no normal, like a vertex, just use an empty
# rotation, which means having the same rotation has the owner part.
rot = FreeCAD.Rotation()
if not utils.isVertex(shape):
self.bbox = shape.BoundBox
else:
bbox = info.Object.ViewObject.getBoundingBox()
if bbox.isValid():
self.bbox = bbox
else:
logger.warn('empty bounding box of part {}'.format(
info.PartName))
self.bbox = FreeCAD.BoundBox(0,0,0,5,5,5)
pla = FreeCAD.Placement(utils.getElementPos(shape),rot)
self.offset = pla.copy()
self.offsetInv = pla.inverse()
self.draggerPlacement = info.Placement.multiply(pla)
self.tracePoint = self.draggerPlacement.Base
self.trace = None
def update(self):
info = getPartInfo(self.parent,self.subname)
self.part = info.Part
pla = info.Placement.multiply(FreeCAD.Placement(self.offset))
logger.trace('part move update {}: {}'.format(objName(self.parent),pla))
self.draggerPlacement = pla
if asm3.gui.AsmCmdManager.Trace and \
self.tracePoint.isEqual(pla.Base,1e5):
if not self.trace:
self.trace = FreeCAD.ActiveDocument.addObject(
'Part::Polygon','AsmTrace')
self.trace.Nodes = {-1:self.tracePoint}
self.tracePoint = pla.Base
self.trace.Nodes = {-1:pla.Base}
self.trace.recompute()
return pla
_undoName = 'Assembly move'
def move(self):
obj = self.assembly.Object
pla = obj.ViewObject.DraggingPlacement
update = True
if self.fixedTransform:
fixed = self.fixedTransform
movement = self.draggerPlacement.inverse().multiply(pla)
if not fixed.Shape:
# The moving part has completly fixed placement, so we move the
# parent assembly instead
pla = obj.Placement.multiply(movement)
setPlacement(obj,pla,self.undos,self._undoName)
update = False
else:
# fixed position, so reset translation
movement.Base = FreeCAD.Vector()
if not utils.isVertex(fixed.Shape):
yaw,_,_ = movement.Rotation.toEuler()
# when dragging with a fixed axis, we align the dragger Z
# axis with that fixed axis. So we shall only keep the yaw
# among the euler angles
movement.Rotation = FreeCAD.Rotation(yaw,0,0)
pla = self.draggerPlacement.multiply(movement)
if update:
# obtain and update the part placement
pla = pla.multiply(self.offsetInv)
setPlacement(self.part,pla,self.undos,self._undoName)
if not asm3.gui.AsmCmdManager.AutoRecompute:
# AsmCmdManager.AutoRecompute means auto re-solve the system. The
# recompute() call below is only for updating linked element and
# stuff
obj.recompute(True)
return
System.touch(obj)
# calls asm3.solver.solve(obj) and redirect all the exceptions message
# to logger only.
logger.catch('solver exception when moving part',
asm3.solver.solve,self.objs)
# self.draggerPlacement, which holds the intended dragger placement, is
# updated by the above solver call through the following chain,
# solver.solve() -> (triggers dependent objects recompute when done)
# Assembly.execute() ->
# ViewProviderAssembly.onExecute() ->
# AsmMovingPart.update()
return self.draggerPlacement
def getMovingPartInfo():
'''Extract information from current selection for part moving
It returns a tuple containing the selected assembly hierarchy (obtained from
Assembly.findChildren()), and AsmPartInfo of the selected child part object.
If there is only one selection, then the moving part will be one belong to
the deepest nested assembly object is selected hierarchy.
If there are two selections, then one selection must be a parent assembly
containing the other child object. The moving object will then be the
immediate child part object of the owner assembly. The actual selected sub
element, i.e. vertex, edge, face will determine the dragger placement
'''
sels = FreeCADGui.Selection.getSelectionEx('',False)
if not sels:
raise RuntimeError('no selection')
if len(sels)>1 or len(sels[0].SubElementNames)>2:
raise RuntimeError('too many selection')
ret = Assembly.findChildren(sels[0].Object,sels[0].SubElementNames[0])
if not ret:
raise RuntimeError('invalid selection {}, subname {}'.format(
objName(sels[0].Object),sels[0].SubElementNames[0]))
if len(sels[0].SubElementNames)==1:
info = getPartInfo(ret[-1].Assembly,ret[-1].Subname)
if not info and len(ret)>1:
info = getPartInfo(ret[-2].Assembly,ret[-2].Subname)
if not info:
return
return (ret, info)
ret2 = Assembly.findChildren(sels[0].Object,sels[0].SubElementNames[1])
if not ret2:
raise RuntimeError('invalid selection {}, subname {}'.format(
objName(sels[0].Object),sels[0].SubElementNames[1]))
if len(ret) == len(ret2):
if not ret2[-1].Object:
ret,ret2 = ret2,ret
elif len(ret) > len(ret2):
ret,ret2 = ret2,ret
assembly = ret[-1].Assembly
for r in ret2:
if assembly == r.Assembly:
return (ret2, getPartInfo(r.Assembly,r.Subname))
raise RuntimeError('not child parent selection')
def canMovePart():
return logger.catchTrace('',getMovingPartInfo) is not None
def movePart(useCenterballDragger=None):
ret = logger.catch('exception when moving part', getMovingPartInfo)
if not ret:
return False
info = ret[1]
doc = FreeCADGui.editDocument()
if doc:
doc.resetEdit()
if isTypeOf(info.Parent,AsmPartGroup):
vobj = info.Parent.Proxy.parent.Object.ViewObject
else:
vobj = info.Parent.ViewObject
if useCenterballDragger is not None:
vobj.UseCenterballDragger = useCenterballDragger
vobj.Proxy._movingPart = AsmMovingPart(*ret)
return vobj.Document.setEdit(vobj,1)
class ViewProviderAssembly(ViewProviderAsmGroup): class ViewProviderAssembly(ViewProviderAsmGroup):
def __init__(self,vobj):
self._movingPart = None
super(ViewProviderAssembly,self).__init__(vobj)
def canDragObject(self,_child): def canDragObject(self,_child):
return False return False
@ -1191,3 +1406,39 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
def getIcon(self): def getIcon(self):
return System.getIcon(self.ViewObject.Object) return System.getIcon(self.ViewObject.Object)
def doubleClicked(self, _vobj):
return movePart()
def onExecute(self):
if not getattr(self,'_movingPart',None):
return
pla = logger.catch('exception when update moving part',
self._movingPart.update)
if pla:
self.ViewObject.DraggingPlacement = pla
else:
doc = FreeCADGui.editDocument()
if doc:
doc.resetEdit()
def initDraggingPlacement(self):
if not getattr(self,'_movingPart',None):
return
return (FreeCADGui.editDocument().EditingTransform,
self._movingPart.draggerPlacement,
self._movingPart.bbox)
def onDragStart(self):
self._movingPart.undos = set()
def onDragMotion(self):
return self._movingPart.move()
def onDragEnd(self):
for doc in self._movingPart.undos:
doc.commitTransaction()
def unsetEdit(self,_vobj,_mode):
self._movingPart = None
return False

17
gui.py
View File

@ -141,26 +141,13 @@ class AsmCmdMove(AsmCmdBase):
_iconName = 'Assembly_Move.svg' _iconName = 'Assembly_Move.svg'
_useCenterballDragger = True _useCenterballDragger = True
@classmethod
def getSelection(cls):
from asm3.assembly import isTypeOf,AsmElementLink
sels = FreeCADGui.Selection.getSelection()
if len(sels)==1 and isTypeOf(sels[0],AsmElementLink):
return sels[0].ViewObject
@classmethod @classmethod
def Activated(cls): def Activated(cls):
vobj = cls.getSelection() asm3.assembly.movePart(cls._useCenterballDragger)
if vobj:
doc = FreeCADGui.editDocument()
if doc:
doc.resetEdit()
vobj.UseCenterballDragger = cls._useCenterballDragger
vobj.doubleClicked()
@classmethod @classmethod
def checkActive(cls): def checkActive(cls):
cls._active = True if cls.getSelection() else False cls._active = asm3.assembly.canMovePart()
@classmethod @classmethod
def onClearSelection(cls): def onClearSelection(cls):