diff --git a/assembly.py b/assembly.py index 057f8be..deae02d 100644 --- a/assembly.py +++ b/assembly.py @@ -28,13 +28,16 @@ def isTypeOf(obj,tp,resolve=False): def checkType(obj,tp,resolve=False): if not isTypeOf(obj,tp,resolve): - raise TypeError('Expect object "{}" to be of type "{}"'.format( + raise TypeError('Expect object {} to be of type "{}"'.format( objName(obj),tp.__name__)) def getProxy(obj,tp): checkType(obj,tp) return obj.Proxy +# For faking selection obtained from Gui.getSelectionEx() +Selection = namedtuple('AsmSelection',('Object','SubElementNames')) + class AsmBase(object): def __init__(self): self.Object = None @@ -86,6 +89,15 @@ class ViewProviderAsmBase(object): if cls._iconName: return utils.getIcon(cls) + def canDropObjects(self): + return True + + def canDragObjects(self): + return False + + def canDragAndDropObject(self,_obj): + return False + class AsmGroup(AsmBase): def linkSetup(self,obj): @@ -115,6 +127,9 @@ class ViewProviderAsmGroup(ViewProviderAsmBase): def doubleClicked(self, _vobj): return False + def canDropObject(self,_child): + return False + class AsmPartGroup(AsmGroup): def __init__(self,parent): @@ -139,12 +154,19 @@ class ViewProviderAsmPartGroup(ViewProviderAsmBase): def onDelete(self,_obj,_subs): return False - def canDropObject(self,obj): + def canDropObjectEx(self,obj,_owner,_subname): return isTypeOf(obj,Assembly) or not isTypeOf(obj,AsmBase) - def canDropObjects(self): + def canDragObject(self,_obj): return True + def canDragObjects(self): + return True + + def canDragAndDropObject(self,_obj): + return True + + class AsmElement(AsmBase): def __init__(self,parent): self.shape = None @@ -237,17 +259,24 @@ class AsmElement(AsmBase): 'The selections must have a common (grand)parent assembly') sel = sels[0] - subs = sel.SubElementNames + subs = list(sel.SubElementNames) + if not subs: + raise RuntimeError('no sub object in selection') if len(subs)>2: raise RuntimeError('At most two selection is allowed.\n' 'The first selection must be a sub element belonging to some ' 'assembly. The optional second selection must be an element ' 'belonging to the same assembly of the first selection') + if len(subs)==2: + if len(subs[0])<len(subs[1]): + subs = [subs[1],subs[2]] - subElement = subs[0].split('.')[-1] - if not subElement: - raise RuntimeError( - 'Please select a sub element belonging to some assembly') + if subs[0][-1] == '.': + subElement = utils.deduceSelectedElement(sel.Object,subs[0]) + if not subElement: + raise RuntimeError('no sub element (face, edge, vertex) in ' + '{}.{}'.format(sel.Object.Name,subs[0])) + subs[0] += subElement link = Assembly.findPartGroup(sel.Object,subs[0]) if not link: @@ -276,14 +305,17 @@ class AsmElement(AsmBase): def make(selection=None,name='Element'): if not selection: selection = AsmElement.getSelection() + if not selection.Subname or selection.Subname[-1]=='.': + raise RuntimeError('Subname must refer to a sub-element') assembly = getProxy(selection.Assembly,Assembly) element = selection.Element if not element: elements = assembly.getElementGroup() # try to search the element group for an existing element for e in elements.Group: - if getProxy(e,AsmElement).getSubName() == selection.Subname: - return element + sub = logger.catch('',e.Proxy.getSubName) + if sub == selection.Subname: + return e element = elements.Document.addObject("App::FeaturePython", name,AsmElement(elements),None,True) ViewProviderAsmElement(element.ViewObject) @@ -307,6 +339,20 @@ class ViewProviderAsmElement(ViewProviderAsmBase): def getDefaultColor(self): return (60.0/255.0,1.0,1.0) + def canDropObjectEx(self,_obj,owner,subname): + # check if is dropping a sub-element + if not subname or subname[-1]=='.': + return False + proxy = self.ViewObject.Object.Proxy + return proxy.getAssembly().getPartGroup()==owner + + def dropObjectEx(self,vobj,_obj,_owner,subname): + obj = vobj.Object + AsmElement.make(AsmElement.Selection( + Assembly=obj.Proxy.getAssembly().Object, + Element=obj, Subname=subname)) + + PartInfo = namedtuple('AsmPartInfo', ('Parent','SubnameRef','Part', 'PartName','Placement','Object','Subname','Shape')) @@ -517,7 +563,9 @@ class AsmElementLink(AsmBase): logger.debug('shape subname {} -> {}'.format(subname,sub)) return sub - def prepareLink(self,owner,subname): + def prepareLink(self,owner,subname,checkOnly=False): + if not owner or not subname: + raise RuntimeError('no owner or subname') assembly = self.getAssembly() sobj = owner.getSubObject(subname,1) if not sobj: @@ -550,6 +598,11 @@ class AsmElementLink(AsmBase): if not isTypeOf(ret[-1].Object,AsmPartGroup): raise RuntimeError('Invalid element link ' + subname) + if checkOnly: + if not ret[-1].Subname or ret[-1].Subname[-1]=='.': + raise RuntimeError('Subname must refer to a sub-element') + return True + # call AsmElement.make to either create a new element, or an existing # element if there is one element = AsmElement.make(AsmElement.Selection( @@ -566,7 +619,16 @@ class AsmElementLink(AsmBase): def setLink(self,owner,subname): obj = self.Object - obj.setLink(*self.prepareLink(owner,subname)) + owner,subname = self.prepareLink(owner,subname) + for sibling in self.parent.Object.Group: + if sibling == self.Object: + continue + linked = sibling.LinkedObject + if isinstance(linked,tuple) and \ + linked[0]==owner and linked[1]==subname: + raise RuntimeError('duplicate element link {} in constraint ' + '{}'.format(objName(sibling),objName(self.parent.Object))) + obj.setLink(owner,subname) linked = obj.getLinkedObject(False) if linked and linked!=obj: label = linked.Label.split('_') @@ -607,7 +669,7 @@ class AsmElementLink(AsmBase): setupUndo(part.Document,undoDocs,undoName) part.Placement = pla - MakeInfo = namedtuple('AsmElementLinkSelection', + MakeInfo = namedtuple('AsmElementLinkMakeInfo', ('Constraint','Owner','Subname')) @staticmethod @@ -627,6 +689,17 @@ class ViewProviderAsmElementLink(ViewProviderAsmBase): def doubleClicked(self,_vobj): return movePart() + def canDropObjectEx(self,_obj,owner,subname): + if logger.catchTrace('Cannot drop to AsmLink {}'.format( + objName(self.ViewObject.Object)), + self.ViewObject.Object.Proxy.prepareLink, + owner, subname, True): + return True + return False + + def dropObjectEx(self,vobj,_obj,owner,subname): + vobj.Object.Proxy.setLink(owner,subname) + class AsmConstraint(AsmGroup): @@ -700,77 +773,100 @@ class AsmConstraint(AsmGroup): return shapes.append(info.Shape) elements.append(o) - Constraint.check(obj,shapes) + Constraint.check(obj,shapes,True) self.elements = elements return self.elements - Selection = namedtuple('ConstraintSelection', + Selection = namedtuple('AsmConstraintSelection', ('SelObject','SelSubname','Assembly','Constraint','Elements')) @staticmethod - def getSelection(typeid=0): + def getSelection(typeid=0,sels=None): ''' Parse Gui.Selection for making a constraint The selected elements must all belong to the same immediate parent assembly. ''' - sels = FreeCADGui.Selection.getSelectionEx('',False) + if not sels: + sels = FreeCADGui.Selection.getSelectionEx('',False) if not sels: raise RuntimeError('no selection') if len(sels)>1: raise RuntimeError( 'The selections must have a common (grand)parent assembly') + subs = sels[0].SubElementNames + if not subs: + raise RuntimeError('no sub-object in selection') + if len(subs)>2: + raise RuntimeError('too many selection') + if len(subs)==2: + sobj = sels[0].Object.getSubObject(subs[1],1) + if isTypeOf(sobj,(AsmConstraintGroup,Assembly,AsmConstraint)): + subs = (subs[1],subs[0]) sel = sels[0] cstr = None elements = [] assembly = None selSubname = None - for sub in sel.SubElementNames: + for sub in subs: sobj = sel.Object.getSubObject(sub,1) if not sobj: - raise RuntimeError('Cannot find sub-object "{}" of {}'.format( - sub,sel.Object)) + raise RuntimeError('Cannot find sub-object {}.{}'.format( + sel.Object.Name,sub)) ret = Assembly.find(sel.Object,sub, recursive=True,relativeToChild=False) if not ret: raise RuntimeError('Selection {}.{} is not from an ' 'assembly'.format(sel.Object.Name,sub)) + + # check if the selection is a constraint group or a constraint + if isTypeOf(sobj,(AsmConstraintGroup,Assembly,AsmConstraint)): + if assembly: + raise RuntimeError('no element selection') + assembly = ret[-1].Assembly + selSubname = sub[:-len(ret[-1].Subname)] + if isTypeOf(sobj,AsmConstraint): + cstr = sobj + continue + if not assembly: - # check if the selection is a constraint group or a constraint - if isTypeOf(sobj,(AsmConstraintGroup,Assembly,AsmConstraint)): - assembly = ret[-1].Assembly - selSubname = sub[:-len(ret[-1].Subname)] - if isTypeOf(sobj,AsmConstraint): - cstr = sobj - continue assembly = ret[0].Assembly selSubname = sub[:-len(ret[0].Subname)] - - found = None - for r in ret: - if r.Assembly == assembly: - found = r - break - if not found: - raise RuntimeError('Selection {}.{} is not from the target ' - 'assembly {}'.format(sel.Object.Name,sub,objName(assembly))) + found = ret[0] + else: + found = None + for r in ret: + if r.Assembly == assembly: + found = r + break + if not found: + raise RuntimeError('Selection {}.{} is not from the target ' + 'assembly {}'.format( + sel.Object.Name,sub,objName(assembly))) # because we call Assembly.find() above with relativeToChild=False, # we shall adjust the element subname by popping the first '.' sub = found.Subname sub = sub[sub.index('.')+1:] + if sub[-1] == '.' and not isTypeOf(sobj,(Assembly,AsmConstraint, + AsmConstraintGroup,AsmElement,AsmElementLink)): + # Too bad, its a full selection, let's guess the sub element + subElement = utils.deduceSelectedElement(found.Object,sub) + if not subElement: + raise RuntimeError('no sub element (face, edge, vertex) in ' + '{}.{}'.format(found.Object.Name,sub)) + sub += subElement + elements.append((found.Object,sub)) - check = None - if cstr and not Constraint.isDisabled(cstr): - typeid = Constraint.getTypeID(cstr) - info = cstr.Proxy.getInfo() - check = [o.getShape() for o in info.Elements] + elements - else: - check = elements - if check: + if not Constraint.isDisabled(cstr): + if cstr: + typeid = Constraint.getTypeID(cstr) + check = [o.Proxy.getInfo().Shape for o in cstr.Group] + elements + else: + check = elements Constraint.check(typeid,check) return AsmConstraint.Selection(SelObject=sel.Object, @@ -780,7 +876,7 @@ class AsmConstraint(AsmGroup): Elements = elements) @staticmethod - def make(typeid, sel=None, name='Constraint', undo=True): + def make(typeid,sel=None,name='Constraint',undo=True): if not sel: sel = AsmConstraint.getSelection(typeid) if sel.Constraint: @@ -795,7 +891,10 @@ class AsmConstraint(AsmGroup): doc.openTransaction('Assembly make constraint') cstr = constraints.Document.addObject("App::FeaturePython", name,AsmConstraint(constraints),None,True) - ViewProviderAsmConstraint(cstr.ViewObject) + proxy = ViewProviderAsmConstraint(cstr.ViewObject) + logger.debug('cstr viewobject {},{},{},{}'.format( + id(proxy),id(cstr.ViewObject.Proxy), + id(proxy.ViewObject),id(cstr.ViewObject))) constraints.setLink({-1:cstr}) Constraint.setTypeID(cstr,typeid) @@ -809,13 +908,15 @@ class AsmConstraint(AsmGroup): if undo: doc.commitTransaction() - FreeCADGui.Selection.clearSelection() - subname = sel.SelSubname - if subname: - subname += '.' - subname += sel.Assembly.Proxy.getConstraintGroup().Name + '.' + \ - cstr.Name + '.' - FreeCADGui.Selection.addSelection(sel.SelObject,subname) + if sel.SelObject: + FreeCADGui.Selection.clearSelection() + subname = sel.SelSubname + if subname: + subname += '.' + subname += sel.Assembly.Proxy.getConstraintGroup().Name + \ + '.' + cstr.Name + '.' + FreeCADGui.Selection.addSelection(sel.SelObject,subname) + FreeCADGui.runCommand('Std_TreeSelection') return cstr except Exception: @@ -837,6 +938,44 @@ class ViewProviderAsmConstraint(ViewProviderAsmGroup): def getIcon(self): return Constraint.getIcon(self.ViewObject.Object) + def _getSelection(self,owner,subname): + if not owner: + raise RuntimeError('no owner') + parent = getattr(owner.Proxy,'parent',None) + if isinstance(parent,AsmConstraintGroup): + # This can happen when we are dropping another element link from the + # same constraint group, in which case, 'owner' here will be the + # parent constraint of the dropping element link + subname = owner.Name + '.' + subname + owner = parent.Object + parent = parent.parent # ascend to the parent assembly + if not isinstance(parent,Assembly): + raise RuntimeError('not from the same assembly') + subname = owner.Name + '.' + subname + obj = self.ViewObject.Object + mysub = parent.getConstraintGroup().Name + '.' + obj.Name + '.' + sel = [Selection(Object=parent.Object,SubElementNames=[subname,mysub])] + typeid = Constraint.getTypeID(obj) + return AsmConstraint.getSelection(typeid,sel) + + def canDropObjectEx(self,_obj,owner,subname): + cstr = self.ViewObject.Object + if logger.catchTrace('Cannot drop to AsmConstraint {}'.format(cstr), + self._getSelection,owner,subname): + return True + return False + + def dropObjectEx(self,_vobj,_obj,owner,subname): + sel = self._getSelection(owner,subname) + cstr = self.ViewObject.Object + typeid = Constraint.getTypeID(cstr) + sel = AsmConstraint.Selection(SelObject=None, + SelSubname=None, + Assembly=sel.Assembly, + Constraint=cstr, + Elements=sel.Elements) + AsmConstraint.make(typeid,sel,undo=False) + class AsmConstraintGroup(AsmGroup): def __init__(self,parent): @@ -864,6 +1003,9 @@ class AsmConstraintGroup(AsmGroup): class ViewProviderAsmConstraintGroup(ViewProviderAsmBase): _iconName = 'Assembly_Assembly_Constraints_Tree.svg' + def canDropObjects(self): + return False + class AsmElementGroup(AsmGroup): def __init__(self,parent): @@ -876,6 +1018,9 @@ class AsmElementGroup(AsmGroup): for o in obj.Group: getProxy(o,AsmElement).parent = self + def getAssembly(self): + return self.parent + @staticmethod def make(parent,name='Elements'): obj = parent.Document.addObject("App::FeaturePython",name, @@ -891,24 +1036,17 @@ class ViewProviderAsmElementGroup(ViewProviderAsmBase): def onDelete(self,_obj,_subs): return False - def canDragObject(self,_obj): - return False - - def canDragObjects(self): - return False - - def canDragAndDropObject(self,_obj): - return False - def canDropObjectEx(self,_obj,owner,subname): # check if is dropping a sub-element - if subname.rfind('.')+1 == len(subname): + if not subname or subname[-1]=='.': return False - return self.ViewObject.Object.Proxy.parent.getPartGroup()==owner + proxy = self.ViewObject.Object.Proxy + return proxy.getAssembly().getPartGroup()==owner def dropObjectEx(self,vobj,_obj,_owner,subname): AsmElement.make(AsmElement.Selection( - vobj.Object.Proxy.parent.Object,None,subname)) + Assembly=vobj.Object.Proxy.getAssembly().Object, + Element=None, Subname=subname)) BuildShapeNone = 'None' @@ -1341,6 +1479,9 @@ def getMovingPartInfo(): if not sels: raise RuntimeError('no selection') + if not sels[0].SubElementNames: + raise RuntimeError('no sub object in selection') + if len(sels)>1 or len(sels[0].SubElementNames)>2: raise RuntimeError('too many selection') @@ -1401,24 +1542,29 @@ class ViewProviderAssembly(ViewProviderAsmGroup): self._movingPart = None super(ViewProviderAssembly,self).__init__(vobj) - def canDragObject(self,_child): - return False + def _convertSubname(self,owner,subname): + sub = subname.split('.') + if not sub: + return + me = self.ViewObject.Object + partGroup = me.Proxy.getPartGroup().ViewObject + if sub == me.Name: + return partGroup,partGroup,subname[len[sub]+1:] + return partGroup,owner,subname - def canDragObjects(self): - return False - - @property - def PartGroup(self): - return self.ViewObject.Object.Proxy.getPartGroup() - - def canDropObject(self,obj): - self.PartGroup.ViewObject.canDropObject(obj) - - def canDropObjects(self): - return True + def canDropObjectEx(self,obj,owner,subname): + info = self._convertSubname(owner,subname) + if not info: + return False + partGroup,owner,subname = info + return partGroup.canDropObject(obj,owner,subname) def dropObjectEx(self,_vobj,obj,owner,subname): - self.PartGroup.ViewObject.dropObject(obj,owner,subname) + info = self._convertSubname(owner,subname) + if not info: + return False + partGroup,owner,subname = info + partGroup.dropObject(obj,owner,subname) def getIcon(self): return System.getIcon(self.ViewObject.Object) diff --git a/constraint.py b/constraint.py index fd07119..8188836 100644 --- a/constraint.py +++ b/constraint.py @@ -218,8 +218,8 @@ class Constraint(ProxyType): return getattr(obj,mcs._disabled,False) @classmethod - def check(mcs,tp,group): - mcs.getType(tp).check(group) + def check(mcs,tp,group,checkCount=False): + mcs.getType(tp).check(group,checkCount) @classmethod def prepare(mcs,obj,solver): @@ -260,9 +260,10 @@ class Constraint(ProxyType): ret = {} for obj in cstrs: cstr = mcs.getProxy(obj) - for info in cstr.getFixedTransform(obj): - found = True - ret[info.Part] = info + if cstr.hasFixedPart(obj): + for info in cstr.getFixedTransform(obj): + found = True + ret[info.Part] = info if not found and not firstPart: elements = obj.Proxy.getElements() @@ -331,32 +332,33 @@ class Base(with_metaclass(Constraint,object)): @classmethod def getEntityDef(cls,group,checkCount,obj=None): entities = cls._entityDef - if len(group) != len(entities): - if not checkCount and len(group)<len(entities): - return entities[:len(group)] - if cls._workplane and len(group)==len(entities)+1: - entities = list(entities) - entities.append(_w) - else: - if not obj: - name = cls.getName() - else: - name += cstrName(obj) - raise RuntimeError('Constraint {} has wrong number of ' - 'elements {}, expecting {}'.format( - name,len(group),len(entities))) - return entities + if len(group) == len(entities): + return entities + if cls._workplane and len(group)==len(entities)+1: + return list(entities) + [_w] + if not checkCount and len(group)<len(entities): + return entities[:len(group)] + if not obj: + name = cls.getName() + else: + name += cstrName(obj) + if len(group)<len(entities): + msg = entities[len(group)](None,None,None,None) + raise RuntimeError('Constraint {} expects a {} element of ' + '{}'.format(name,_ordinal[len(group)],msg)) + raise RuntimeError('Constraint {} has too many elements, expecting ' + 'only {}'.format(name,len(entities))) @classmethod - def check(cls,group): - entities = cls.getEntityDef(group,False) + def check(cls,group,checkCount=False): + entities = cls.getEntityDef(group,checkCount) for i,e in enumerate(entities): o = group[i] msg = e(None,None,None,o) if not msg: continue if i == len(cls._entityDef): - raise RuntimeError('Constraint "{}" requires the optional {} ' + raise RuntimeError('Constraint "{}" requires an optional {} ' 'element to be a planar face for defining a ' 'workplane'.format(cls.getName(), _ordinal[i], msg)) raise RuntimeError('Constraint "{}" requires the {} element to be' @@ -475,7 +477,7 @@ class Locked(Base): return ret @classmethod - def check(cls,group): + def check(cls,group,_checkCount=False): if not all([utils.isElement(o) for o in group]): raise RuntimeError('Constraint "{}" requires all children to be ' 'of element (Vertex, Edge or Face)'.format(cls.getName())) @@ -486,7 +488,7 @@ class BaseMulti(Base): _entityDef = (_wa,) @classmethod - def check(cls,group): + def check(cls,group,_checkCount=False): if len(group)<2: raise RuntimeError('Constraint "{}" requires at least two ' 'elements'.format(cls.getName())) diff --git a/utils.py b/utils.py index eb1c2b1..01d226c 100644 --- a/utils.py +++ b/utils.py @@ -61,6 +61,22 @@ def isLine(param): else: return isinstance(param,Part.Line) +def deduceSelectedElement(obj,subname): + shape = obj.getSubObject(subname) + if not shape: + return + count = len(shape.Faces) + if count==1: + return 'Face1' + elif not count: + count = len(shape.Edges) + if count==1: + return 'Edge1' + elif not count: + count = len(shape.Vertexes) + if count==1: + return 'Vertex1' + def getElement(obj,tp): if isinstance(obj,tuple): obj = obj[0].getSubObject(obj[1])