From ad35683c87a5873d927d7872a626301d529b64b7 Mon Sep 17 00:00:00 2001 From: "Zheng, Lei" Date: Sat, 21 Jul 2018 18:24:37 +0800 Subject: [PATCH] Add support for constraint multiplication --- assembly.py | 458 ++++++++++++++++++++++++++++++++++++++++++++------ constraint.py | 72 ++++++-- gui.py | 32 +++- utils.py | 10 +- 4 files changed, 497 insertions(+), 75 deletions(-) diff --git a/assembly.py b/assembly.py index 10b8675..92b365e 100644 --- a/assembly.py +++ b/assembly.py @@ -266,10 +266,10 @@ class AsmElement(AsmBase): obj.addProperty("App::PropertyBool","LinkTransform"," Link",'') obj.LinkTransform = True if not hasattr(obj,'Detach'): - obj.addProperty('App::PropertyLink','Detach', ' Link','') + obj.addProperty('App::PropertyBool','Detach', ' Link','') obj.setPropertyStatus('LinkTransform',['Immutable','Hidden']) - obj.configLinkProperty('LinkedObject','Placement','LinkTransform') obj.setPropertyStatus('LinkedObject','ReadOnly') + obj.configLinkProperty('LinkedObject','Placement','LinkTransform') def attach(self,obj): obj.addProperty("App::PropertyXLink","LinkedObject"," Link",'') @@ -299,15 +299,33 @@ class AsmElement(AsmBase): parent.Object.cacheChildLabel() if prop not in _IgnoredProperties and \ not Constraint.isDisabled(parent.Object): - Assembly.autoSolve() + Assembly.autoSolve(obj,prop) def execute(self,obj): info = None if not obj.Detach and hasattr(obj,'Shape'): info = getElementInfo(self.getAssembly().getPartGroup(), self.getElementSubname()) - shape = info.Shape - shape.transformShape(info.Placement.toMatrix(),True) + mat = info.Placement.toMatrix() + if not getattr(obj,'Radius',None): + shape = Part.Shape(info.Shape) + shape.transformShape(mat,True) + else: + if isinstance(info.Part,tuple): + parentShape = Part.getShape(info.Part[2], info.Subname, + transform=info.Part[3], needSubElement=False) + else: + parentShape = Part.getShape(info.Part, info.Subname, + transform=False, needSubElement=False) + shapes = [] + for edge in parentShape.Edges: + if info.Shape.isCoplanar(edge) and \ + utils.isSameValue( + utils.getElementCircular(edge,True),obj.Radius): + edge.transformShape(mat,True) + shapes.append(edge) + shape = shapes + # make a compound to keep the shape's transformation shape = Part.makeCompound(shape) shape.ElementMap = info.Shape.ElementMap @@ -347,8 +365,37 @@ class AsmElement(AsmBase): objName(self.Object))) return link[1] - def getElementSubname(self): - return self.getSubName() + def getElementSubname(self,recursive=False): + ''' + Recursively resolve the geometry element link relative to the parent + assembly's part group + ''' + + subname = self.getSubName() + if not recursive: + return subname + + obj = self.Object.getLinkedObject(False) + if not obj or obj == self.Object: + raise RuntimeError('Borken element link') + if not isTypeOf(obj,AsmElement): + # If not pointing to another element, then assume we are directly + # pointing to the geometry element, just return as it is, which is a + # subname relative to the parent assembly part group + return subname + + childElement = obj.Proxy + + # If pointing to another element in the child assembly, first pop two + # names in the subname reference, i.e. element label and element group + # name + idx = subname.rfind('.',0,subname.rfind('.',0,-1)) + subname = subname[:idx+1] + + # append the child assembly part group name, and recursively call into + # child element + return subname+childElement.getAssembly().getPartGroup().Name+'.'+\ + childElement.getElementSubname(True) # Element: optional, if none, then a new element will be created if no # pre-existing. Or else, it shall be the element to be amended @@ -398,6 +445,8 @@ class AsmElement(AsmBase): subElement = utils.deduceSelectedElement(sel.Object,subs[0]) if subElement: subs[0] += subElement + else: + subElement = '' link = Assembly.findPartGroup(sel.Object,subs[0]) if not link: @@ -431,7 +480,7 @@ class AsmElement(AsmBase): return element @staticmethod - def make(selection=None,name='Element',undo=False): + def make(selection=None,name='Element',undo=False,radius=None): '''Add/get/modify an element with the given selected object''' if not selection: selection = AsmElement.getSelection() @@ -487,7 +536,8 @@ class AsmElement(AsmBase): # then import that element to the current assembly. sel = AsmElement.Selection(Element=None, Group=ret.Object, Subname=ret.Subname) - element = AsmElement.make(sel) + element = AsmElement.make(sel,radius=radius) + radius=None # now generate the subname reference @@ -526,9 +576,15 @@ class AsmElement(AsmBase): if not e.Offset.isIdentity(): continue sub = logger.catch('',e.Proxy.getSubName) - if sub == subname: + if sub!=subname: + continue + r = getattr(e,'Radius',None) + if (not radius and not r) or radius==r: return e element = AsmElement.create(name,elements) + if radius: + element.addProperty('App::PropertyFloat','Radius','','') + element.Radius = radius elements.setLink({idx:element}) elements.setElementVisible(element.Name,False) element.Proxy._initializing = False @@ -641,7 +697,8 @@ class ViewProviderAsmElementSketch(ViewProviderAsmElement): ElementInfo = namedtuple('AsmElementInfo', ('Parent','SubnameRef','Part', 'PartName','Placement','Object','Subname','Shape')) -def getElementInfo(parent,subname,checkPlacement=False,shape=None): +def getElementInfo(parent,subname, + checkPlacement=False,shape=None,recursive=False): '''Return a named tuple containing the part object element information Parameters: @@ -662,8 +719,8 @@ def getElementInfo(parent,subname,checkPlacement=False,shape=None): 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, + Part: either the part object, or a tuple(array,idx,element,collapsed) to + refer to an element in an link array, PartName: a string name for the part @@ -694,7 +751,7 @@ def getElementInfo(parent,subname,checkPlacement=False,shape=None): objName(parent), subname)) if not isTypeOf(child,(AsmElement,AsmElementLink)): raise RuntimeError('{} cannot be moved'.format(objName(child))) - subname = child.Proxy.getElementSubname() + subname = child.Proxy.getElementSubname(recursive) names = subname.split('.') partGroup = parent.Proxy.getPartGroup() @@ -749,7 +806,7 @@ def getElementInfo(parent,subname,checkPlacement=False,shape=None): shape=utils.getElementShape( (part[1],subname),transform=False) pla = part[1].Placement - obj = part[0].getLinkedObject(False) + obj = part[1].getLinkedObject(False) partName = part[1].Name idx = int(partName.split('_i')[-1]) part = (part[0],idx,part[1],False) @@ -811,14 +868,19 @@ class AsmElementLink(AsmBase): def __init__(self,parent): super(AsmElementLink,self).__init__() self.info = None + self.infos = [] self.part = None self.parent = getProxy(parent,AsmConstraint) def linkSetup(self,obj): super(AsmElementLink,self).linkSetup(obj) - obj.configLinkProperty('LinkedObject') obj.setPropertyStatus('LinkedObject','ReadOnly') + obj.configLinkProperty('LinkedObject') + if hasattr(obj,'Count'): + obj.configLinkProperty('PlacementList', + 'ShowElement',ElementCount='Count') self.info = None + self.infos = [] self.part = None def attach(self,obj): @@ -838,19 +900,32 @@ class AsmElementLink(AsmBase): self.parent.Object,oldPart,info.Part,info.PartName) return False + _MyIgnoredProperties = _IgnoredProperties | \ + set(('AcountCount','PlacementList')) + def onChanged(self,obj,prop): if obj.Removing or \ not getattr(self,'parent',None) or \ FreeCAD.isRestoring(): return - if prop not in _IgnoredProperties and \ + if prop == 'Count': + self.infos *= 0 # clear the list + self.info = None + return + if prop == 'NoExpand': + cstr = self.parent.Object + if obj!=cstr.Group[0] and cstr.Multiply and obj.LinkedObject: + self.setLink(self.getAssembly().getPartGroup(), + self.getElementSubname(True)) + return + if prop not in self._MyIgnoredProperties and \ not Constraint.isDisabled(self.parent.Object): - Assembly.autoSolve() + Assembly.autoSolve(obj,prop) def getAssembly(self): return self.parent.parent.parent - def getElementSubname(self): + def getElementSubname(self,recursive=False): 'Resolve element link subname' # AsmElementLink is used by constraint to link to a geometry link. It @@ -861,13 +936,14 @@ class AsmElementLink(AsmBase): # the AsmElementLink's subname reference to the actual part object # subname reference relative to the parent assembly's part group - linked = self.Object.getLinkedObject(False) - if not linked or linked == self.Object: + link = self.Object.LinkedObject + linked = link[0].getSubObject(link[1],retType=1) + if not linked: raise RuntimeError('Element link broken') element = getProxy(linked,AsmElement) assembly = element.getAssembly() if assembly == self.getAssembly(): - return element.getElementSubname() + return element.getElementSubname(recursive) # The reference stored inside this ElementLink. We need the sub-assembly # name, which is the name before the first dot. This name may be @@ -879,23 +955,66 @@ class AsmElementLink(AsmBase): ref = self.Object.LinkedObject[1] prefix = ref[0:ref.rfind('.',0,ref.rfind('.',0,-1))] return '{}.{}.{}'.format(prefix, assembly.getPartGroup().Name, - element.getElementSubname()) + element.getElementSubname(recursive)) + + def setLink(self,owner,subname,checkOnly=False,multiply=False): + obj = self.Object + cstr = self.parent.Object + elements = cstr.Group + radius = None + if (multiply or Constraint.canMultiply(cstr)) and \ + obj!=elements[0] and \ + not getattr(obj,'NoExpand',None): + + info = getElementInfo(owner,subname) + + radius = utils.getElementCircular(info.Shape,True) + if radius and not checkOnly and not hasattr(obj,'NoExpand'): + touched = 'Touched' in obj.State + obj.addProperty('App::PropertyBool','NoExpand','', + 'Disable auto inclusion of coplanar edges '\ + 'with the same radius') + if len(elements)>2 and getattr(elements[-2],'NoExpand',None): + obj.NoExpand = True + radius = None + if not touched: + obj.purgeTouched() + if radius: + if isinstance(info.Part,tuple): + parentShape = Part.getShape(info.Part[2], info.Subname, + transform=info.Part[3], needSubElement=False) + else: + parentShape = Part.getShape(info.Part, info.Subname, + transform=False, needSubElement=False) + count = 0 + for edge in parentShape.Edges: + if not info.Shape.isCoplanar(edge) or \ + not utils.isSameValue( + utils.getElementCircular(edge,True),radius): + continue + count += 1 + if count > 1: + break + if count<=1: + radius = None + + if checkOnly: + return True - def setLink(self,owner,subname,checkOnly=False): # check if there is any sub-assembly in the reference ret = Assembly.find(owner,subname) if not ret: # if not, add/get an element in our own element group sel = AsmElement.Selection(Element=None, Group=owner, Subname=subname) - element = AsmElement.make(sel) + element = AsmElement.make(sel,radius=radius) owner = element.Proxy.parent.Object subname = '${}.'.format(element.Label) else: # if so, add/get an element from the sub-assembly sel = AsmElement.Selection(Element=None, Group=ret.Object, Subname=ret.Subname) - element = AsmElement.make(sel) + element = AsmElement.make(sel,radius=radius) owner = owner.Proxy.getAssembly().getPartGroup() # This give us reference to child assembly's immediate child @@ -909,29 +1028,84 @@ class AsmElementLink(AsmBase): subname = '{}.${}.'.format(prefix, element.Label) - for sibling in self.parent.Object.Group: - if sibling == self.Object: + for sibling in elements: + if sibling == obj: 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))) - if not checkOnly: - self.Object.setLink(owner,subname) + '{}'.format(objName(sibling),objName(cstr))) + obj.setLink(owner,subname) - def getInfo(self,refresh=False): + def getInfo(self,refresh=False,expand=False): if not refresh: - ret = getattr(self,'info',None) - if ret: - return ret + if expand: + info = getattr(self,'infos',None) + if info: + return info + info = getattr(self,'info',None) + if info: + return [info] if expand else info + self.info = None + self.infos *= 0 # clear the list obj = getattr(self,'Object',None) if not obj: return + + shape = obj.LinkedObject[0].getSubObject(obj.LinkedObject[1]) self.info = getElementInfo(self.getAssembly().getPartGroup(), - self.getElementSubname(),shape=obj.getSubObject('')) - return self.info + self.getElementSubname(),shape=shape) + info = self.info + + parent = self.parent.Object + if not Constraint.canMultiply(parent): + self.infos.append(info) + return self.infos if expand else self.info + + if obj == parent.Group[0]: + if not isinstance(info.Part,tuple) or \ + getLinkProperty(info.Part[0],'ElementCount')!=obj.Count: + self.infos.append(info) + return self.infos if expand else self.info + infos = [] + offset = info.Placement.inverse() + plaList = [] + for i in xrange(obj.Count): + part = info.Part + if part[3]: + pla = getLinkProperty(part[0],'PlacementList')[i] + part = (part[0],i,part[2],part[3]) + else: + sobj = part[0].getSubObject(str(i)+'.',retType=1) + pla = sobj.Placement + part = (part[0],i,sobj,part[3]) + plaList.append(pla.multiply(offset)) + infos.append(ElementInfo(Parent = info.Parent, + SubnameRef = info.SubnameRef, + Part=part, + PartName = '{}.{}'.format(part[0].Name,i), + Placement = pla.copy(), + Object = info.Object, + Subname = info.Subname, + Shape = shape)) + obj.PlacementList = plaList + self.infos = infos + return infos if expand else info + + for i,edge in enumerate(info.Shape.Edges): + self.infos.append(ElementInfo( + Parent = info.Parent, + SubnameRef = info.SubnameRef, + Part = info.Part, + PartName = info.PartName, + Placement = info.Placement, + Object = info.Object, + Subname = '{}_{}'.format(info.Subname,i), + Shape = edge)) + + return self.infos if expand else self.info @staticmethod def setPlacement(part,pla): @@ -979,6 +1153,9 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop): super(ViewProviderAsmElementLink,self).attach(vobj) vobj.OnTopWhenSelected = 2 + def claimChildren(self): + return [] + def getDefaultColor(self): return (1.0,60.0/255.0,60.0/255.0) @@ -987,7 +1164,7 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop): return mover.movePart() def canDropObjectEx(self,_obj,owner,subname,elements): - if len(elements)>1: + if len(elements)>1 or not owner: return False elif elements: subname += elements[0] @@ -1034,14 +1211,17 @@ class AsmConstraint(AsmGroup): if not assembly or \ System.isConstraintSupported(assembly,Constraint.getTypeName(obj)): return - raise RuntimeError('Constraint type "{}" is not supported by ' + logger.err('Constraint type "{}" is not supported by ' 'solver "{}"'.format(Constraint.getTypeName(obj), System.getTypeName(assembly))) + Constraint.setDisable(obj) def onChanged(self,obj,prop): if not obj.Removing and prop not in _IgnoredProperties: - Constraint.onChanged(obj,prop) - Assembly.autoSolve() + if prop == Constraint.propMultiply() and not FreeCAD.isRestoring(): + self.checkMultiply() + Constraint.onChanged(obj,prop) + Assembly.autoSolve(obj,prop) def linkSetup(self,obj): self.elements = None @@ -1057,10 +1237,65 @@ class AsmConstraint(AsmGroup): Constraint.attach(obj) obj.recompute() - def execute(self,_obj): + def checkMultiply(self): + obj = self.Object + if not obj.Multiply: + return + children = obj.Group + if len(children)<=1: + return + count = 0 + for e in children[1:]: + info = e.Proxy.getInfo(True) + count += info.Shape.countElement('Edge') + + firstChild = children[0] + info = firstChild.Proxy.getInfo() + if not isinstance(info.Part,tuple): + 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.addProperty("App::PropertyBool","AutoCount",'', + 'Auto change part count to match constraining elements') + firstChild.AutoCount = True + firstChild.addProperty("App::PropertyPlacementList", + "PlacementList",'','') + firstChild.setPropertyStatus('PlacementList','Output') + firstChild.addProperty("App::PropertyBool","ShowElement",'','') + firstChild.setPropertyStatus('ShowElement',('Hidden','Immutable')) + firstChild.configLinkProperty('PlacementList', + 'ShowElement',ElementCount='Count') + + if firstChild.AutoCount: + if getLinkProperty(info.Part[0],'ElementCount',None,True) is None: + firstChild.AutoCount = False + else: + partTouched = 'Touched' in info.Part[0].State + setLinkProperty(info.Part[0],'ElementCount',count) + if not partTouched: + info.Part[0].purgeTouched() + + if not firstChild.AutoCount: + count = getLinkProperty(info.Part[0],'ElementCount') + + if firstChild.Count != count: + firstChild.Count = count + + if not touched and 'Touched' in firstChild.State: + # purge touched to avoid recomputation multi-pass + firstChild.purgeTouched() + firstChild.Proxy.getInfo(True) + + def execute(self,obj): if not getattr(self,'_initializing',False) and\ getattr(self,'parent',None): self.checkSupport() + if Constraint.canMultiply(obj): + self.checkMultiply() self.getElements(True) return False @@ -1073,16 +1308,32 @@ class AsmConstraint(AsmGroup): ret = getattr(self,'elements',None) if ret or Constraint.isDisabled(obj): return ret + elementInfo = [] elements = [] - for o in obj.Group: - checkType(o,AsmElementLink) - info = o.Proxy.getInfo() - if not info: - return - elementInfo.append(info) - elements.append(o) - Constraint.check(obj,elementInfo,True) + group = obj.Group + if Constraint.canMultiply(obj): + firstInfo = group[0].Proxy.getInfo(expand=True) + if not firstInfo: + raise RuntimeError('invalid first element') + elements.append(group[0]) + for o in group[1:]: + info = o.Proxy.getInfo(expand=True) + if not info: + continue + elementInfo += info + elements.append(o) + for info in zip(firstInfo,elementInfo[:len(firstInfo)]): + Constraint.check(obj,info,True) + else: + for o in group: + checkType(o,AsmElementLink) + info = o.Proxy.getInfo() + if not info: + return + elementInfo.append(info) + elements.append(o) + Constraint.check(obj,elementInfo,True) self.elements = elements return self.elements @@ -1185,7 +1436,7 @@ class AsmConstraint(AsmGroup): elementInfo.append(getElementInfo( assembly,found.Object.Name+'.'+sub)) - if not Constraint.isDisabled(cstr): + if not Constraint.isDisabled(cstr) and not Constraint.canMultiply(cstr): if cstr: typeid = Constraint.getTypeID(cstr) check = [] @@ -1224,7 +1475,14 @@ class AsmConstraint(AsmGroup): for e in sel.Elements: AsmElementLink.make(AsmElementLink.MakeInfo(cstr,*e)) logger.catchDebug('init constraint', Constraint.init,cstr) + + if gui.AsmCmdManager.AutoElementVis: + cstr.setPropertyStatus('VisibilityList','-Immutable') + cstr.VisibilityList = [False]*len(cstr.Group) + cstr.setPropertyStatus('VisibilityList','Immutable') + cstr.Proxy._initializing = False + if undo: FreeCAD.closeActiveTransaction() undo = False @@ -1241,11 +1499,97 @@ class AsmConstraint(AsmGroup): FreeCADGui.runCommand('Std_TreeSelection') return cstr - except Exception: + except Exception as e: + logger.debug('failed to make constraint: {}'.format(e)) if undo: FreeCAD.closeActiveTransaction(True) raise + @staticmethod + def makeMultiply(checkOnly=False): + sels = FreeCADGui.Selection.getSelection() + if not len(sels)==1 or not isTypeOf(sels[0],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: + 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') + + try: + if not checkOnly: + FreeCAD.setActiveTransaction("Assembly constraint multiply") + + partGroup = cstr.Proxy.getAssembly().getPartGroup() + + if multiplied: + subs = elements[0].Proxy.getElementSubname(True).split('.') + infos0 = [] + for i in xrange(elements[0].Count): + subs[1] = str(i) + infos0.append((partGroup,'.'.join(subs))) + infos = [] + for element in elements[1:]: + if element.NoExpand: + infos.append(element.LinkedObject) + continue + info = element.Proxy.getInfo() + subs = Part.splitSubname( + element.Proxy.getElementSubname(True)) + if isinstance(info.Part,tuple): + subs[0] = '{}.{}'.format(info.Part[1],subs[0]) + parentShape = Part.getShape( + partGroup,subs[0],noElementMap=True) + subShape = parentShape.getElement(subs[2]) + radius = utils.getElementCircular(subShape,True) + for i,edge in enumerate(parentShape.Edges): + if subShape.isCoplanar(edge) and \ + utils.isSameValue( + utils.getElementCircular(edge,True),radius): + subs[2] = 'Edge{}'.format(i+1) + subs[1] = parentShape.getElementName(subs[2]) + 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)]): + sel = AsmConstraint.Selection(SelObject=None, + SelSubname=None, + Assembly = assembly, + Constraint = None, + Elements = info) + AsmConstraint.make(typeid,sel,undo=False) + 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 + if elements[0].AutoCount and \ + getLinkProperty(info.Part[0],'ShowElement',None,True): + setLinkProperty(info.Part[0],'ShowElement',False) + FreeCAD.closeActiveTransaction() + return True + except Exception: + if not checkOnly: + FreeCAD.closeActiveTransaction(True) + raise + class ViewProviderAsmConstraint(ViewProviderAsmGroup): def attach(self,vobj): @@ -1930,19 +2274,21 @@ class Assembly(AsmGroup): except Exception: del partMap[obj] else: - cls.autoSolve(True) + cls.autoSolve(obj,prop,True) @classmethod - def autoSolve(cls,force=False): + def autoSolve(cls,obj,prop,force=False): if force or cls.canAutoSolve(): if not cls._Timer.isSingleShot(): cls._Timer.setSingleShot(True) cls._Timer.timeout.connect(Assembly.onSolverTimer) - logger.debug('auto solve scheduled',frame=1) + logger.debug('auto solve scheduled on change of {}.{}'.format( + objName(obj),prop),frame=1) cls._Timer.start(300) @classmethod def cancelAutoSolve(cls): + logger.debug('cancel auto solve',frame=1) cls._Timer.stop() @classmethod @@ -2131,7 +2477,7 @@ class Assembly(AsmGroup): return if prop!='Group' and prop not in _IgnoredProperties: System.onChanged(obj,prop) - Assembly.autoSolve() + Assembly.autoSolve(obj,prop) def onDocumentRestored(self,obj): super(Assembly,self).onDocumentRestored(obj) diff --git a/constraint.py b/constraint.py index 81ac988..cfeb56d 100644 --- a/constraint.py +++ b/constraint.py @@ -96,7 +96,7 @@ def _p(solver,partInfo,subname,shape,retAll=False): system.NameTag = nameTag + 't' h = system.addTransform(e,*partInfo.Params,group=partInfo.Group) h = PointInfo(entity=h, params=partInfo.Params,vector=v) - system.log('{}: {},{}'.format(key,h,partInfo.Group)) + system.log('{}: {},{}'.format(system.NameTag,h,partInfo.Group)) partInfo.EntityMap[key] = h return h if retAll else h.entity @@ -142,7 +142,7 @@ def _n(solver,partInfo,subname,shape,retAll=False): h = NormalInfo(entity=nz,rot=rot, params=partInfo.Params, p0=p0.entity, ln=ln) - system.log('{}: {},{}'.format(key,h,partInfo.Group)) + system.log('{}: {},{}'.format(system.NameTag,h,partInfo.Group)) partInfo.EntityMap[key] = h return h if retAll else h.entity @@ -194,7 +194,7 @@ def _l(solver,partInfo,subname,shape,retAll=False): system.NameTag = nameTag h = system.addLineSegment(tp0,tp1,group=partInfo.Group) h = LineInfo(entity=h,p0=tp0,p1=tp1) - system.log('{}: {},{}'.format(key,h,partInfo.Group)) + system.log('{}: {},{}'.format(system.NameTag,h,partInfo.Group)) partInfo.EntityMap[key] = h return h if retAll else h.entity @@ -256,7 +256,7 @@ def _w(solver,partInfo,subname,shape,retAll=False): system.NameTag = partInfo.PartName + '.' + key w = system.addWorkplane(p.entity,n.entity,group=partInfo.Group) h = PlaneInfo(entity=w,origin=p,normal=n) - system.log('{}: {},{}'.format(key,h,partInfo.Group)) + system.log('{}: {},{}'.format(system.NameTag,h,partInfo.Group)) return h if retAll else h.entity def _wa(solver,partInfo,subname,shape,retAll=False): @@ -311,7 +311,7 @@ def _c(solver,partInfo,subname,shape,requireArc=False,retAll=False): e = system.addCircle(pln.origin.entity, pln.normal.entity, system.addDistance(r), group=g) h = CircleInfo(entity=e,radius=r,p0=p0) - system.log('{}: add draft circle {}, {}'.format(key,h,g)) + system.log('{}: add draft circle {}, {}'.format(nameTag,h,g)) else: system.NameTag = nameTag + '.c' center = system.addPoint2d(pln.entity,solver.v0,solver.v0,group=g) @@ -328,7 +328,7 @@ def _c(solver,partInfo,subname,shape,requireArc=False,retAll=False): system.NameTag = nameTag e = system.addArcOfCircle(pln.entity,center,*points,group=g) h = ArcInfo(entity=e,p1=points[1],p0=points[0],params=params) - system.log('{}: add draft arc {}, {}'.format(key,h,g)) + system.log('{}: add draft arc {}, {}'.format(nameTag,h,g)) # exhaust all possible keys from a draft circle to save # recomputation @@ -352,7 +352,7 @@ def _c(solver,partInfo,subname,shape,requireArc=False,retAll=False): h = system.addCircle( pln.origin.entity, pln.normal.entity, hr, group=g) h = CircleInfo(entity=h,radius=hr,p0=None) - system.log('{}: {},{}'.format(key,h,g)) + system.log('{}: {},{}'.format(nameTag,h,g)) partInfo.EntityMap[key] = h @@ -461,6 +461,18 @@ class Constraint(ProxyType): def isDisabled(mcs,obj): return getattr(obj,mcs._disabled,False) + @classmethod + def propMultiply(mcs): + return 'Multiply' + + @classmethod + def canMultiply(mcs,obj): + return getattr(obj,mcs.propMultiply(),None) + + @classmethod + def setDisable(mcs,obj): + return setattr(obj,mcs._disabled,True) + @classmethod def check(mcs,tp,elements,checkCount=False): mcs.getType(tp).check(elements,checkCount) @@ -552,6 +564,7 @@ _makeProp('Offset','App::PropertyDistance',getter=propGetValue) _makeProp('OffsetX','App::PropertyDistance',getter=propGetValue) _makeProp('OffsetY','App::PropertyDistance',getter=propGetValue) _makeProp('Cascade','App::PropertyBool',internal=True) +_makeProp('Multiply','App::PropertyBool',internal=True) _makeProp('Angle','App::PropertyAngle',getter=propGetValue) _AngleProps = [ @@ -846,7 +859,7 @@ class BaseMulti(Base): msg = cls._entityDef[0](None,info.Part,info.Subname,info.Shape) if msg: raise RuntimeError('Constraint "{}" requires all the element ' - 'to be of {}'.format(cls.getName())) + 'to be of {}'.format(cls.getName(),msg)) return @classmethod @@ -855,10 +868,45 @@ class BaseMulti(Base): if not func: logger.warn('{} no constraint func'.format(cstrName(obj))) return + props = cls.getPropertyValues(obj) + ret = [] + + if cls.canMultiply(obj): + elements = obj.Proxy.getElements() + if len(elements)<=1: + logger.warn('{} not enough elements'.format(cstrName(obj))) + return + + firstInfo = elements[0].Proxy.getInfo(expand=True) + count = len(firstInfo) + if not count: + logger.warn('{} no first part shape'.format(cstrName(obj))) + return + idx = 0 + for element in elements[1:]: + for info in element.Proxy.getInfo(expand=True): + info0 = firstInfo[idx] + partInfo0 = solver.getPartInfo(info0) + partInfo = solver.getPartInfo(info) + e0 = cls._entityDef[0]( + solver,partInfo0,info0.Subname,info0.Shape) + e = cls._entityDef[0]( + solver,partInfo,info.Subname,info.Shape) + params = props + [e0,e] + solver.system.checkRedundancy(obj,partInfo0,partInfo) + h = func(*params,group=solver.group) + if isinstance(h,(list,tuple)): + ret += list(h) + else: + ret.append(h) + idx += 1 + if idx >= count: + return ret + return ret + parts = set() ref = None elements = [] - props = cls.getPropertyValues(obj) for e in obj.Proxy.getElements(): info = e.Proxy.getInfo() @@ -880,7 +928,6 @@ class BaseMulti(Base): logger.warn('{} has no effective constraint'.format(cstrName(obj))) return e0 = None - ret = [] firstInfo = None for e in elements: info = e.Proxy.getInfo() @@ -899,7 +946,6 @@ class BaseMulti(Base): ret.append(h) return ret - class BaseCascade(BaseMulti): @classmethod def prepare(cls,obj,solver): @@ -941,7 +987,7 @@ class BaseCascade(BaseMulti): class PlaneCoincident(BaseCascade): _id = 35 _iconName = 'Assembly_ConstraintCoincidence.svg' - _props = ['Cascade','Offset','OffsetX','OffsetY'] + _AngleProps + _props = ['Multiply','Cascade','Offset','OffsetX','OffsetY'] + _AngleProps _tooltip = \ 'Add a "{}" constraint to conincide planes of two or more parts.\n'\ 'The planes are coincided at their centers with an optional distance.' @@ -958,7 +1004,7 @@ class AxialAlignment(BaseMulti): _id = 36 _entityDef = (_lna,) _iconName = 'Assembly_ConstraintAxial.svg' - _props = _AngleProps + _props = ['Multiply'] + _AngleProps _tooltip = 'Add a "{}" constraint to align planes of two or more parts.\n'\ 'The planes are aligned at the direction of their surface normal axis.' diff --git a/gui.py b/gui.py index ec61f51..15ca2e3 100644 --- a/gui.py +++ b/gui.py @@ -258,7 +258,7 @@ class AsmCmdMove(AsmCmdBase): @classmethod def checkActive(cls): - cls._active = logger.catchTrace('',cls.canMove) + cls._active = True if logger.catchTrace('',cls.canMove) else False @classmethod def onClearSelection(cls): @@ -628,3 +628,33 @@ class AsmCmdDown(AsmCmdUp): @classmethod def Activated(cls): cls.move(1) + + +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.' + _iconName = 'Assembly_ConstraintMultiply.svg' + + @classmethod + def checkActive(cls): + from .assembly import AsmConstraint + if logger.catchTrace('',AsmConstraint.makeMultiply,True): + cls._active = True + else: + cls._active = False + + @classmethod + def Activated(cls): + from .assembly import AsmConstraint + AsmConstraint.makeMultiply() + + @classmethod + def onClearSelection(cls): + cls._active = False diff --git a/utils.py b/utils.py index 9095392..3c1aa79 100644 --- a/utils.py +++ b/utils.py @@ -90,7 +90,7 @@ def getElementShape(obj,tp=None,transform=False,noElementMap=True): logger.trace('no sub object {}'.format(obj)) return if shape.isNull(): - if sobj.TypeId == 'App::Line': + if sobj.isDerivedFrom('App::Line'): if tp not in (None,Part.Shape,Part.Edge): logger.trace('wrong type of shape {}'.format(obj)) return @@ -99,7 +99,7 @@ def getElementShape(obj,tp=None,transform=False,noElementMap=True): FreeCAD.Vector(size,0,0)) shape.transformShape(mat,False,True) return shape - elif sobj.TypeId == 'App::Plane': + elif sobj.isDerivedFrom('App::Plane'): if tp not in (None, Part.Shape, Part.Face): logger.trace('wrong type of shape {}'.format(obj)) return @@ -418,7 +418,7 @@ def getElementsAngle(o1,o2,pla1=None,pla2=None): v2 = getElementDirection(o2,pla2) return math.degrees(v1.getAngle(v2)) -def getElementCircular(obj): +def getElementCircular(obj,radius=False): 'return radius if it is closed, or a list of two endpoints' edge = getElementShape(obj,Part.Edge) if not edge: @@ -427,7 +427,7 @@ def getElementCircular(obj): return c = edge.Curve if hasattr( c, 'Radius' ): - if edge.Closed: + if radius or edge.Closed: return c.Radius elif isLine(edge.Curve): return @@ -437,7 +437,7 @@ def getElementCircular(obj): arc = BSpline.toBiArcs(10**-6)[0] except Exception: #FreeCAD exception thrown () return - if edge.Closed: + if radius or edge.Closed: return arc[0].Radius return [v.Point for v in edge.Vertexes]