From ce3193946ad80765dec58e17f90d0a1c96918bbf Mon Sep 17 00:00:00 2001 From: "Zheng, Lei" Date: Sun, 19 May 2019 11:40:44 +0800 Subject: [PATCH] assembly: improve constraint mutliplication It can now auto change any part into link array if necessary. --- assembly.py | 208 +++++++++++++++++++++++++++++++++++----------------- gui.py | 11 ++- 2 files changed, 144 insertions(+), 75 deletions(-) diff --git a/assembly.py b/assembly.py index 4b636db..aac47f8 100644 --- a/assembly.py +++ b/assembly.py @@ -27,7 +27,7 @@ def getProxy(obj,tp): def getLinkProperty(obj,name,default=None,writable=False): try: - obj = obj.getLinkedObject(True) + # obj = obj.getLinkedObject(True) if not writable: return obj.getLinkExtProperty(name) name = obj.getLinkExtPropertyName(name) @@ -38,7 +38,7 @@ def getLinkProperty(obj,name,default=None,writable=False): return default def setLinkProperty(obj,name,val): - obj = obj.getLinkedObject(True) + # obj = obj.getLinkedObject(True) setattr(obj,obj.getLinkExtPropertyName(name),val) def flattenSubname(obj,subname): @@ -46,7 +46,7 @@ def flattenSubname(obj,subname): Falttern any AsmPlainGroups inside subname path. Only the first encountered assembly along the subname path is considered ''' - + func = getattr(obj,'flattenSubname',None) if not func: return subname @@ -406,6 +406,19 @@ class ViewProviderAsmPartGroup(ViewProviderAsmGroup): cvp.ForceMapColors = True vobj.DefaultMode = mode + def replaceObject(self,oldObj,newObj): + res = self.ViewObject.replaceObject(oldObj,newObj) + if res<=0: + return res + for obj in oldObj.InList: + if isTypeOf(obj,AsmElement): + link = obj.LinkedObject + if isinstance(link,tuple): + obj.setLink(newObj,link[1]) + else: + obj.setLink(newObj) + return 1 + class AsmVersion(object): def __init__(self,v=None): @@ -877,7 +890,7 @@ class AsmElement(AsmBase): # it is, after making sure it is referencing an sub-element if not utils.isElement((group,subname)): raise RuntimeError( 'Element must reference a geometry ' - 'element {} {}'.format(objName(group),subname)) + 'element {}.{}'.format(objName(group),subname)) else: # In case there are intermediate assembly inside subname, we'll # recursively export the element in child assemblies first, and @@ -1236,41 +1249,42 @@ def getElementInfo(parent,subname, # special treatment of link array (i.e. when ElementCount!=0), we # allow the array element to be moveable by the solver if getLinkProperty(part,'ElementCount'): + + # Handle old element reference before this link is expanded to + # array. if not names[1]: names[1] = '0' names.append('') + elif len(names) == 2: + names.insert(1,'0') # store both the part (i.e. the link array), and the array # element object part = (part,part.getSubObject(names[1]+'.',1)) + if not part[1]: + raise RuntimeError('Cannot find part array element {}.{}.', + part.Name,names[1]) # trim the subname to be after the array element subname = '.'.join(names[2:]) + if not shape: + shape=utils.getElementShape((part[1],subname)) # There are two states of an link array. if getLinkProperty(part[0],'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. - if not shape: - shape=utils.getElementShape( - (part[1],subname),transform=False) + # is a property of the array element. pla = part[0].Placement.multiply(part[1].Placement) obj = part[1].getLinkedObject(False) - partName = part[1].Name + partName = objName(part[1]) idx = int(partName.split('_i')[-1]) part = (part[0],idx,part[1],False) else: plaList = getLinkProperty(part[0],'PlacementList',None,True) if plaList: # 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. must set 'transform' to True. - if not shape: - shape=utils.getElementShape( - (part[1],subname),transform=True) + # is stored inside link object's PlacementList property. obj = part[1] try: if names[1] == part[1].Name: @@ -1287,7 +1301,7 @@ def getElementInfo(parent,subname, raise RuntimeError('invalid array subname of element ' '{}: {}'.format(objName(parent),subnameRef)) - partName = '{}.{}.'.format(part[0].Name,idx) + partName = '{}.{}.'.format(objName(part[0]),idx) if not obj: part = partSaved @@ -1438,26 +1452,26 @@ class AsmElementLink(AsmBase): not getattr(self,'parent',None) or \ FreeCAD.isRestoring(): return - if obj.Document and getattr(obj.Document,'Transacting',False): + elif obj.Document and getattr(obj.Document,'Transacting',False): self.infos *= 0 # clear the list self.info = None return - if prop == 'Count': + elif prop == 'Count': self.infos *= 0 # clear the list self.info = None return - if prop == 'Offset': + elif prop == 'Offset': self.getInfo(True) return - if prop == 'NoExpand': + elif prop == 'NoExpand': cstr = self.parent.Object - if obj!=flattenGroup(cstr)[0] \ + if obj!=cstr.Group[0] \ and cstr.Multiply \ and obj.LinkedObject: self.setLink(self.getAssembly().getPartGroup(), self.getElementSubname(True)) return - if prop == 'Label': + elif prop == 'Label': if obj.Document and getattr(obj.Document,'Transacting',False): return link = getattr(obj,'LinkedObject',None) @@ -1471,6 +1485,9 @@ class AsmElementLink(AsmBase): # re-lable it. obj.Label = linked.Label return + elif prop == 'AutoCount': + if obj.AutoCount and hasattr(obj,'ShowElement'): + self.parent.checkMultiply() if prop not in self._MyIgnoredProperties and \ not Constraint.isDisabled(self.parent.Object): Assembly.autoSolve(obj,prop) @@ -1634,7 +1651,7 @@ class AsmElementLink(AsmBase): return self.infos if expand else self.info self.multiply = True - if obj == flattenGroup(parent)[0]: + if obj == parent.Group[0]: if not isinstance(info.Part,tuple) or \ getLinkProperty(info.Part[0],'ElementCount')!=obj.Count: self.infos.append(info) @@ -1653,10 +1670,11 @@ class AsmElementLink(AsmBase): part = (part[0],i,sobj,part[3]) pla = part[0].Placement.multiply(pla) plaList.append(pla.multiply(offset)) - infos.append(ElementInfo(Parent = info.Parent, + infos.append(ElementInfo( + Parent = info.Parent, SubnameRef = info.SubnameRef, Part=part, - PartName = '{}.{}'.format(part[0].Name,i), + PartName = '{}.{}'.format(objName(part[0]),i), Placement = pla, Object = info.Object, Subname = info.Subname, @@ -1778,7 +1796,7 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop): class AsmConstraint(AsmGroup): def __init__(self,parent): - self.prevOrder = None + self.prevOrder = [] self.version = None self._initializing = True self.elements = None @@ -1813,6 +1831,7 @@ class AsmConstraint(AsmGroup): def onChanged(self,obj,prop): if obj.Document and getattr(obj.Document,'Transacting',False): + Constraint.onChanged(obj,prop) return if not obj.Removing and prop not in _IgnoredProperties: if prop == Constraint.propMultiply() and not FreeCAD.isRestoring(): @@ -1844,7 +1863,11 @@ class AsmConstraint(AsmGroup): obj = self.Object if not obj.Multiply: return - children = flattenGroup(obj) + + if getattr(obj,'Cascade',False): + obj.Cascade = False + + children = obj.Group if len(children)<=1: return count = 0 @@ -1896,13 +1919,13 @@ class AsmConstraint(AsmGroup): firstChild = children[0] info = firstChild.Proxy.getInfo() if not isinstance(info.Part,tuple): - raise RuntimeError('Expect part {} to be an array for' + raise RuntimeError('Expect part {} to be an array for ' 'constraint multiplication'.format(info.PartName)) touched = 'Touched' in firstChild.State if not hasattr(firstChild,'Count'): firstChild.addProperty("App::PropertyInteger","Count",'','') - firstChild.setPropertyStatus('Count',('ReadOnly','Output')) + firstChild.setPropertyStatus('Count','ReadOnly') firstChild.addProperty("App::PropertyBool","AutoCount",'', 'Auto change part count to match constraining elements') firstChild.AutoCount = True @@ -1931,7 +1954,7 @@ class AsmConstraint(AsmGroup): if firstChild.Count != count: firstChild.Count = count - firstChild.Proxy.getInfo(True) + firstChild.recompute() if not touched and 'Touched' in firstChild.State: # purge touched to avoid recomputation multi-pass @@ -2016,6 +2039,7 @@ class AsmConstraint(AsmGroup): # initial placement partGroup = self.getAssembly().getPartGroup() + touched = False for i,info0 in enumerate(infos0): if not distances[i]: continue @@ -2042,10 +2066,13 @@ class AsmConstraint(AsmGroup): else: pla = info0.Placement.multiply(pla.multiply(pla0.inverse())) showPart(partGroup,info0.Part) - info0.Placement.Rotation = pla.Rotation - info0.Placement.Base = pla.Base + touched = True setPlacement(info0.Part,pla,True) + if touched: + firstChild.Proxy.getInfo(True) + firstChild.purgeTouched() + def execute(self,obj): if not getattr(self,'_initializing',False) and\ getattr(self,'parent',None): @@ -2085,7 +2112,6 @@ class AsmConstraint(AsmGroup): elements.append(o) if count <= len(infos): infos = infos[:count] - o.Proxy.infos = infos elementInfo += infos break elementInfo += infos @@ -2280,37 +2306,93 @@ class AsmConstraint(AsmGroup): @staticmethod def makeMultiply(checkOnly=False): - sels = FreeCADGui.Selection.getSelection() - if not len(sels)==1 or not isTypeOf(sels[0],AsmConstraint): + sel = FreeCADGui.Selection.getSelectionEx('*',0) + if len(sel)!=1 or len(sel[0].SubElementNames)!=1: + raise RuntimeError('Too many selections') + + sel = sel[0] + cstr = sel.Object.getSubObject(sel.SubElementNames[0],1) + if not isTypeOf(cstr,AsmConstraint): raise RuntimeError('Must select a constraint') - cstr = sels[0] + multiplied = Constraint.canMultiply(cstr) if multiplied is None: raise RuntimeError('Constraint do not support multiplication') elements = cstr.Proxy.getElements() - if len(elements)<=1: + if len(elements)<2: raise RuntimeError('Constraint must have more than one element') - info = elements[0].Proxy.getInfo() - if not isinstance(info.Part,tuple) or info.Part[1]!=0: - raise RuntimeError('Constraint multiplication requires the first ' - 'element to be from the first element of a link array') + if checkOnly: + return True try: - if not checkOnly: - FreeCAD.setActiveTransaction("Assembly constraint multiply") + FreeCAD.setActiveTransaction("Assembly constraint multiply") + + info = elements[0].Proxy.getInfo() + if not isinstance(info.Part,tuple): + # The first element must be an link array in order to get + # multiplied. + + #First, check if it is a link (with element count) + if getLinkProperty(info.Part,'ElementCount') is None: + # No. So we replace it with a link with command + # Std_LinkReplace, which requires a select of the object + # to be replaced first. So construct the selection path + # by replacing the last two subnames (i.e. + # Constraints.Constraint) with PartGroup.PartName + + subs = flattenSubname(sel.Object,sel.SubElementNames[0]) + subs = subs.split('.') + # The last entry is for sub-element name (e.g. Edge1, + # Face2), which should be empty + subs[-1] = '' + subs[-2] = info.Part.Name + subs[-3] = '2' + # remember last selection + FreeCADGui.Selection.pushSelStack() + FreeCADGui.Selection.clearSelection() + FreeCADGui.Selection.addSelection( + sel.Object,'.'.join(subs)) + FreeCADGui.Selection.pushSelStack() + FreeCADGui.runCommand('Std_LinkReplace') + # restore the last selection + FreeCADGui.runCommand('Std_SelBack') + + info = elements[0].Proxy.getInfo(True) + # make sure the replace command works + if getLinkProperty(info.Part,'ElementCount') is None: + raise RuntimeError('Failed to replace part with a link') + + # Let's first make an single element array without showing + # its element object, which will make the linked object + # grouped under the link rather than floating under tree + # view root + setLinkProperty(info.Part,'ShowElement',False) + try: + setLinkProperty(info.Part,'ElementCount',1) + except Exception: + raise RuntimeError('Failed to change element count of ' + '{}'.format(info.PartName)) partGroup = cstr.Proxy.getAssembly().getPartGroup() - if multiplied: - cstr.recompute(True) + cstr.recompute(True) + + if not multiplied: + for elementLink in elements[1:]: + subname = elementLink.Proxy.getElementSubname(True) + elementLink.Proxy.setLink( + partGroup,subname,checkOnly,multiply=True) + cstr.Multiply = True + else: + # Here means the constraint is already multiplied, expand it to + # multiple individual constraints elements = cstr.Proxy.getElements() - subs = elements[0].Proxy.getElementSubname(True).split('.') - infos0 = [] - for info0 in elements[0].Proxy.getInfo(expand=True): - subs[1] = str(info0.Part[1]) - infos0.append((partGroup,'.'.join(subs))) + infos0 = [(partGroup,'{}.{}.{}'.format(info.Part[0].Name, + info.Part[1], + info.Subname)) \ + for info in elements[0].Proxy.getInfo(expand=True)] infos = [] for element in elements[1:]: if element.NoExpand: @@ -2334,8 +2416,6 @@ class AsmConstraint(AsmGroup): if subs[1] == subs[2]: subs[1] = '' infos.append((partGroup,Part.joinSubname(*subs))) - if checkOnly: - return True assembly = cstr.Proxy.getAssembly().Object typeid = Constraint.getTypeID(cstr) for info in zip(infos0,infos[:len(infos0)]): @@ -2346,23 +2426,14 @@ class AsmConstraint(AsmGroup): Elements = info) newCstr = AsmConstraint.make(typeid,sel,undo=False) Constraint.copy(cstr,newCstr) - for element,target in zip(elements,flattenGroup(newCstr)): + for element,target in zip(elements,newCstr.Group): target.Offset = element.Offset cstr.Document.removeObject(cstr.Name) - FreeCAD.closeActiveTransaction() - return True - for elementLink in elements[1:]: - subname = elementLink.Proxy.getElementSubname(True) - elementLink.Proxy.setLink( - partGroup,subname,checkOnly,multiply=True) - if not checkOnly: - cstr.Multiply = True - FreeCAD.closeActiveTransaction() + FreeCAD.closeActiveTransaction() return True except Exception: - if not checkOnly: - FreeCAD.closeActiveTransaction(True) + FreeCAD.closeActiveTransaction(True) raise @@ -2464,7 +2535,6 @@ class AsmConstraintGroup(AsmGroup): if obj.Document and getattr(obj.Document,'Transacting',False): return if prop not in _IgnoredProperties: - System.onChanged(obj,prop) Assembly.autoSolve(obj,prop) @staticmethod @@ -3008,8 +3078,8 @@ class AsmRelation(AsmBase): else: part = obj.Part group = [] - for cstr in self.getAssembly().getConstraintGroup().LinkedChildren: - for element in flattenGroup(cstr): + for cstr in flattenGroup(self.getAssembly().getConstraintGroup()): + for element in cstr.Group: info = element.Proxy.getInfo() if isinstance(info.Part,tuple): infoPart = info.Part[:2] @@ -3501,7 +3571,7 @@ class Assembly(AsmGroup): continue if not System.isConstraintSupported(self.Object, Constraint.getTypeName(o)): - logger.debug('skip unsupported constraint ' + logger.warn('skip unsupported constraint ' '{}',cstrName(o)) continue ret.append(o) diff --git a/gui.py b/gui.py index e994acc..53224a5 100644 --- a/gui.py +++ b/gui.py @@ -1038,12 +1038,11 @@ class AsmCmdMultiply(AsmCmdBase): _id = 18 _menuText = 'Multiply constraint' _tooltip = 'Mutiply the part owner of the first element to constrain\n'\ - 'against the rest of the elements.\n\n'\ - 'To activate this function, the FIRST part must be of the\n'\ - 'FIRST element of a link array. In will optionally expand\n'\ - 'colplanar circular edges with the same radius in the second\n'\ - 'element on wards. To disable auto expansion, use NoExpand\n'\ - 'property in the element link.' + 'against the rest of the elements. It will auto replace the\n'\ + 'first part owner with a link array when necessary.\n\n'\ + 'It will also optionally expand colplanar circular edges with\n'\ + 'the same radius in the second element on wards. To disable\n'\ + 'auto expansion, use NoExpand property in the element link.' _iconName = 'Assembly_ConstraintMultiply.svg' @classmethod