Add support for constraint multiplication

This commit is contained in:
Zheng, Lei 2018-07-21 18:24:37 +08:00
parent 4c9cc3033e
commit ad35683c87
4 changed files with 497 additions and 75 deletions

View File

@ -266,10 +266,10 @@ class AsmElement(AsmBase):
obj.addProperty("App::PropertyBool","LinkTransform"," Link",'') obj.addProperty("App::PropertyBool","LinkTransform"," Link",'')
obj.LinkTransform = True obj.LinkTransform = True
if not hasattr(obj,'Detach'): if not hasattr(obj,'Detach'):
obj.addProperty('App::PropertyLink','Detach', ' Link','') obj.addProperty('App::PropertyBool','Detach', ' Link','')
obj.setPropertyStatus('LinkTransform',['Immutable','Hidden']) obj.setPropertyStatus('LinkTransform',['Immutable','Hidden'])
obj.configLinkProperty('LinkedObject','Placement','LinkTransform')
obj.setPropertyStatus('LinkedObject','ReadOnly') obj.setPropertyStatus('LinkedObject','ReadOnly')
obj.configLinkProperty('LinkedObject','Placement','LinkTransform')
def attach(self,obj): def attach(self,obj):
obj.addProperty("App::PropertyXLink","LinkedObject"," Link",'') obj.addProperty("App::PropertyXLink","LinkedObject"," Link",'')
@ -299,15 +299,33 @@ class AsmElement(AsmBase):
parent.Object.cacheChildLabel() parent.Object.cacheChildLabel()
if prop not in _IgnoredProperties and \ if prop not in _IgnoredProperties and \
not Constraint.isDisabled(parent.Object): not Constraint.isDisabled(parent.Object):
Assembly.autoSolve() Assembly.autoSolve(obj,prop)
def execute(self,obj): def execute(self,obj):
info = None info = None
if not obj.Detach and hasattr(obj,'Shape'): if not obj.Detach and hasattr(obj,'Shape'):
info = getElementInfo(self.getAssembly().getPartGroup(), info = getElementInfo(self.getAssembly().getPartGroup(),
self.getElementSubname()) self.getElementSubname())
shape = info.Shape mat = info.Placement.toMatrix()
shape.transformShape(info.Placement.toMatrix(),True) 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 # make a compound to keep the shape's transformation
shape = Part.makeCompound(shape) shape = Part.makeCompound(shape)
shape.ElementMap = info.Shape.ElementMap shape.ElementMap = info.Shape.ElementMap
@ -347,8 +365,37 @@ class AsmElement(AsmBase):
objName(self.Object))) objName(self.Object)))
return link[1] return link[1]
def getElementSubname(self): def getElementSubname(self,recursive=False):
return self.getSubName() '''
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 # 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 # 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]) subElement = utils.deduceSelectedElement(sel.Object,subs[0])
if subElement: if subElement:
subs[0] += subElement subs[0] += subElement
else:
subElement = ''
link = Assembly.findPartGroup(sel.Object,subs[0]) link = Assembly.findPartGroup(sel.Object,subs[0])
if not link: if not link:
@ -431,7 +480,7 @@ class AsmElement(AsmBase):
return element return element
@staticmethod @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''' '''Add/get/modify an element with the given selected object'''
if not selection: if not selection:
selection = AsmElement.getSelection() selection = AsmElement.getSelection()
@ -487,7 +536,8 @@ class AsmElement(AsmBase):
# then import that element to the current assembly. # then import that element to the current assembly.
sel = AsmElement.Selection(Element=None, sel = AsmElement.Selection(Element=None,
Group=ret.Object, Subname=ret.Subname) Group=ret.Object, Subname=ret.Subname)
element = AsmElement.make(sel) element = AsmElement.make(sel,radius=radius)
radius=None
# now generate the subname reference # now generate the subname reference
@ -526,9 +576,15 @@ class AsmElement(AsmBase):
if not e.Offset.isIdentity(): if not e.Offset.isIdentity():
continue continue
sub = logger.catch('',e.Proxy.getSubName) 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 return e
element = AsmElement.create(name,elements) element = AsmElement.create(name,elements)
if radius:
element.addProperty('App::PropertyFloat','Radius','','')
element.Radius = radius
elements.setLink({idx:element}) elements.setLink({idx:element})
elements.setElementVisible(element.Name,False) elements.setElementVisible(element.Name,False)
element.Proxy._initializing = False element.Proxy._initializing = False
@ -641,7 +697,8 @@ class ViewProviderAsmElementSketch(ViewProviderAsmElement):
ElementInfo = namedtuple('AsmElementInfo', ('Parent','SubnameRef','Part', ElementInfo = namedtuple('AsmElementInfo', ('Parent','SubnameRef','Part',
'PartName','Placement','Object','Subname','Shape')) '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 '''Return a named tuple containing the part object element information
Parameters: Parameters:
@ -662,8 +719,8 @@ def getElementInfo(parent,subname,checkPlacement=False,shape=None):
SubnameRef: set to the input subname reference SubnameRef: set to the input subname reference
Part: either the part object, or a tuple(obj, idx) to refer to an element in Part: either the part object, or a tuple(array,idx,element,collapsed) to
an link array, refer to an element in an link array,
PartName: a string name for the part PartName: a string name for the part
@ -694,7 +751,7 @@ def getElementInfo(parent,subname,checkPlacement=False,shape=None):
objName(parent), subname)) objName(parent), subname))
if not isTypeOf(child,(AsmElement,AsmElementLink)): if not isTypeOf(child,(AsmElement,AsmElementLink)):
raise RuntimeError('{} cannot be moved'.format(objName(child))) raise RuntimeError('{} cannot be moved'.format(objName(child)))
subname = child.Proxy.getElementSubname() subname = child.Proxy.getElementSubname(recursive)
names = subname.split('.') names = subname.split('.')
partGroup = parent.Proxy.getPartGroup() partGroup = parent.Proxy.getPartGroup()
@ -749,7 +806,7 @@ def getElementInfo(parent,subname,checkPlacement=False,shape=None):
shape=utils.getElementShape( shape=utils.getElementShape(
(part[1],subname),transform=False) (part[1],subname),transform=False)
pla = part[1].Placement pla = part[1].Placement
obj = part[0].getLinkedObject(False) obj = part[1].getLinkedObject(False)
partName = part[1].Name partName = part[1].Name
idx = int(partName.split('_i')[-1]) idx = int(partName.split('_i')[-1])
part = (part[0],idx,part[1],False) part = (part[0],idx,part[1],False)
@ -811,14 +868,19 @@ class AsmElementLink(AsmBase):
def __init__(self,parent): def __init__(self,parent):
super(AsmElementLink,self).__init__() super(AsmElementLink,self).__init__()
self.info = None self.info = None
self.infos = []
self.part = None self.part = None
self.parent = getProxy(parent,AsmConstraint) self.parent = getProxy(parent,AsmConstraint)
def linkSetup(self,obj): def linkSetup(self,obj):
super(AsmElementLink,self).linkSetup(obj) super(AsmElementLink,self).linkSetup(obj)
obj.configLinkProperty('LinkedObject')
obj.setPropertyStatus('LinkedObject','ReadOnly') obj.setPropertyStatus('LinkedObject','ReadOnly')
obj.configLinkProperty('LinkedObject')
if hasattr(obj,'Count'):
obj.configLinkProperty('PlacementList',
'ShowElement',ElementCount='Count')
self.info = None self.info = None
self.infos = []
self.part = None self.part = None
def attach(self,obj): def attach(self,obj):
@ -838,19 +900,32 @@ class AsmElementLink(AsmBase):
self.parent.Object,oldPart,info.Part,info.PartName) self.parent.Object,oldPart,info.Part,info.PartName)
return False return False
_MyIgnoredProperties = _IgnoredProperties | \
set(('AcountCount','PlacementList'))
def onChanged(self,obj,prop): def onChanged(self,obj,prop):
if obj.Removing or \ if obj.Removing or \
not getattr(self,'parent',None) or \ not getattr(self,'parent',None) or \
FreeCAD.isRestoring(): FreeCAD.isRestoring():
return 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): not Constraint.isDisabled(self.parent.Object):
Assembly.autoSolve() Assembly.autoSolve(obj,prop)
def getAssembly(self): def getAssembly(self):
return self.parent.parent.parent return self.parent.parent.parent
def getElementSubname(self): def getElementSubname(self,recursive=False):
'Resolve element link subname' 'Resolve element link subname'
# AsmElementLink is used by constraint to link to a geometry link. It # 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 # the AsmElementLink's subname reference to the actual part object
# subname reference relative to the parent assembly's part group # subname reference relative to the parent assembly's part group
linked = self.Object.getLinkedObject(False) link = self.Object.LinkedObject
if not linked or linked == self.Object: linked = link[0].getSubObject(link[1],retType=1)
if not linked:
raise RuntimeError('Element link broken') raise RuntimeError('Element link broken')
element = getProxy(linked,AsmElement) element = getProxy(linked,AsmElement)
assembly = element.getAssembly() assembly = element.getAssembly()
if assembly == self.getAssembly(): if assembly == self.getAssembly():
return element.getElementSubname() return element.getElementSubname(recursive)
# The reference stored inside this ElementLink. We need the sub-assembly # The reference stored inside this ElementLink. We need the sub-assembly
# name, which is the name before the first dot. This name may be # 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] ref = self.Object.LinkedObject[1]
prefix = ref[0:ref.rfind('.',0,ref.rfind('.',0,-1))] prefix = ref[0:ref.rfind('.',0,ref.rfind('.',0,-1))]
return '{}.{}.{}'.format(prefix, assembly.getPartGroup().Name, 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 # check if there is any sub-assembly in the reference
ret = Assembly.find(owner,subname) ret = Assembly.find(owner,subname)
if not ret: if not ret:
# if not, add/get an element in our own element group # if not, add/get an element in our own element group
sel = AsmElement.Selection(Element=None, Group=owner, sel = AsmElement.Selection(Element=None, Group=owner,
Subname=subname) Subname=subname)
element = AsmElement.make(sel) element = AsmElement.make(sel,radius=radius)
owner = element.Proxy.parent.Object owner = element.Proxy.parent.Object
subname = '${}.'.format(element.Label) subname = '${}.'.format(element.Label)
else: else:
# if so, add/get an element from the sub-assembly # if so, add/get an element from the sub-assembly
sel = AsmElement.Selection(Element=None, Group=ret.Object, sel = AsmElement.Selection(Element=None, Group=ret.Object,
Subname=ret.Subname) Subname=ret.Subname)
element = AsmElement.make(sel) element = AsmElement.make(sel,radius=radius)
owner = owner.Proxy.getAssembly().getPartGroup() owner = owner.Proxy.getAssembly().getPartGroup()
# This give us reference to child assembly's immediate child # This give us reference to child assembly's immediate child
@ -909,29 +1028,84 @@ class AsmElementLink(AsmBase):
subname = '{}.${}.'.format(prefix, element.Label) subname = '{}.${}.'.format(prefix, element.Label)
for sibling in self.parent.Object.Group: for sibling in elements:
if sibling == self.Object: if sibling == obj:
continue continue
linked = sibling.LinkedObject linked = sibling.LinkedObject
if isinstance(linked,tuple) and \ if isinstance(linked,tuple) and \
linked[0]==owner and linked[1]==subname: linked[0]==owner and linked[1]==subname:
raise RuntimeError('duplicate element link {} in constraint ' raise RuntimeError('duplicate element link {} in constraint '
'{}'.format(objName(sibling),objName(self.parent.Object))) '{}'.format(objName(sibling),objName(cstr)))
if not checkOnly: obj.setLink(owner,subname)
self.Object.setLink(owner,subname)
def getInfo(self,refresh=False): def getInfo(self,refresh=False,expand=False):
if not refresh: if not refresh:
ret = getattr(self,'info',None) if expand:
if ret: info = getattr(self,'infos',None)
return ret if info:
return info
info = getattr(self,'info',None)
if info:
return [info] if expand else info
self.info = None self.info = None
self.infos *= 0 # clear the list
obj = getattr(self,'Object',None) obj = getattr(self,'Object',None)
if not obj: if not obj:
return return
shape = obj.LinkedObject[0].getSubObject(obj.LinkedObject[1])
self.info = getElementInfo(self.getAssembly().getPartGroup(), self.info = getElementInfo(self.getAssembly().getPartGroup(),
self.getElementSubname(),shape=obj.getSubObject('')) self.getElementSubname(),shape=shape)
return self.info 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 @staticmethod
def setPlacement(part,pla): def setPlacement(part,pla):
@ -979,6 +1153,9 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop):
super(ViewProviderAsmElementLink,self).attach(vobj) super(ViewProviderAsmElementLink,self).attach(vobj)
vobj.OnTopWhenSelected = 2 vobj.OnTopWhenSelected = 2
def claimChildren(self):
return []
def getDefaultColor(self): def getDefaultColor(self):
return (1.0,60.0/255.0,60.0/255.0) return (1.0,60.0/255.0,60.0/255.0)
@ -987,7 +1164,7 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop):
return mover.movePart() return mover.movePart()
def canDropObjectEx(self,_obj,owner,subname,elements): def canDropObjectEx(self,_obj,owner,subname,elements):
if len(elements)>1: if len(elements)>1 or not owner:
return False return False
elif elements: elif elements:
subname += elements[0] subname += elements[0]
@ -1034,14 +1211,17 @@ class AsmConstraint(AsmGroup):
if not assembly or \ if not assembly or \
System.isConstraintSupported(assembly,Constraint.getTypeName(obj)): System.isConstraintSupported(assembly,Constraint.getTypeName(obj)):
return return
raise RuntimeError('Constraint type "{}" is not supported by ' logger.err('Constraint type "{}" is not supported by '
'solver "{}"'.format(Constraint.getTypeName(obj), 'solver "{}"'.format(Constraint.getTypeName(obj),
System.getTypeName(assembly))) System.getTypeName(assembly)))
Constraint.setDisable(obj)
def onChanged(self,obj,prop): def onChanged(self,obj,prop):
if not obj.Removing and prop not in _IgnoredProperties: if not obj.Removing and prop not in _IgnoredProperties:
Constraint.onChanged(obj,prop) if prop == Constraint.propMultiply() and not FreeCAD.isRestoring():
Assembly.autoSolve() self.checkMultiply()
Constraint.onChanged(obj,prop)
Assembly.autoSolve(obj,prop)
def linkSetup(self,obj): def linkSetup(self,obj):
self.elements = None self.elements = None
@ -1057,10 +1237,65 @@ class AsmConstraint(AsmGroup):
Constraint.attach(obj) Constraint.attach(obj)
obj.recompute() 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\ if not getattr(self,'_initializing',False) and\
getattr(self,'parent',None): getattr(self,'parent',None):
self.checkSupport() self.checkSupport()
if Constraint.canMultiply(obj):
self.checkMultiply()
self.getElements(True) self.getElements(True)
return False return False
@ -1073,16 +1308,32 @@ class AsmConstraint(AsmGroup):
ret = getattr(self,'elements',None) ret = getattr(self,'elements',None)
if ret or Constraint.isDisabled(obj): if ret or Constraint.isDisabled(obj):
return ret return ret
elementInfo = [] elementInfo = []
elements = [] elements = []
for o in obj.Group: group = obj.Group
checkType(o,AsmElementLink) if Constraint.canMultiply(obj):
info = o.Proxy.getInfo() firstInfo = group[0].Proxy.getInfo(expand=True)
if not info: if not firstInfo:
return raise RuntimeError('invalid first element')
elementInfo.append(info) elements.append(group[0])
elements.append(o) for o in group[1:]:
Constraint.check(obj,elementInfo,True) 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 self.elements = elements
return self.elements return self.elements
@ -1185,7 +1436,7 @@ class AsmConstraint(AsmGroup):
elementInfo.append(getElementInfo( elementInfo.append(getElementInfo(
assembly,found.Object.Name+'.'+sub)) assembly,found.Object.Name+'.'+sub))
if not Constraint.isDisabled(cstr): if not Constraint.isDisabled(cstr) and not Constraint.canMultiply(cstr):
if cstr: if cstr:
typeid = Constraint.getTypeID(cstr) typeid = Constraint.getTypeID(cstr)
check = [] check = []
@ -1224,7 +1475,14 @@ class AsmConstraint(AsmGroup):
for e in sel.Elements: for e in sel.Elements:
AsmElementLink.make(AsmElementLink.MakeInfo(cstr,*e)) AsmElementLink.make(AsmElementLink.MakeInfo(cstr,*e))
logger.catchDebug('init constraint', Constraint.init,cstr) 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 cstr.Proxy._initializing = False
if undo: if undo:
FreeCAD.closeActiveTransaction() FreeCAD.closeActiveTransaction()
undo = False undo = False
@ -1241,11 +1499,97 @@ class AsmConstraint(AsmGroup):
FreeCADGui.runCommand('Std_TreeSelection') FreeCADGui.runCommand('Std_TreeSelection')
return cstr return cstr
except Exception: except Exception as e:
logger.debug('failed to make constraint: {}'.format(e))
if undo: if undo:
FreeCAD.closeActiveTransaction(True) FreeCAD.closeActiveTransaction(True)
raise 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): class ViewProviderAsmConstraint(ViewProviderAsmGroup):
def attach(self,vobj): def attach(self,vobj):
@ -1930,19 +2274,21 @@ class Assembly(AsmGroup):
except Exception: except Exception:
del partMap[obj] del partMap[obj]
else: else:
cls.autoSolve(True) cls.autoSolve(obj,prop,True)
@classmethod @classmethod
def autoSolve(cls,force=False): def autoSolve(cls,obj,prop,force=False):
if force or cls.canAutoSolve(): if force or cls.canAutoSolve():
if not cls._Timer.isSingleShot(): if not cls._Timer.isSingleShot():
cls._Timer.setSingleShot(True) cls._Timer.setSingleShot(True)
cls._Timer.timeout.connect(Assembly.onSolverTimer) 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) cls._Timer.start(300)
@classmethod @classmethod
def cancelAutoSolve(cls): def cancelAutoSolve(cls):
logger.debug('cancel auto solve',frame=1)
cls._Timer.stop() cls._Timer.stop()
@classmethod @classmethod
@ -2131,7 +2477,7 @@ class Assembly(AsmGroup):
return return
if prop!='Group' and prop not in _IgnoredProperties: if prop!='Group' and prop not in _IgnoredProperties:
System.onChanged(obj,prop) System.onChanged(obj,prop)
Assembly.autoSolve() Assembly.autoSolve(obj,prop)
def onDocumentRestored(self,obj): def onDocumentRestored(self,obj):
super(Assembly,self).onDocumentRestored(obj) super(Assembly,self).onDocumentRestored(obj)

View File

@ -96,7 +96,7 @@ def _p(solver,partInfo,subname,shape,retAll=False):
system.NameTag = nameTag + 't' system.NameTag = nameTag + 't'
h = system.addTransform(e,*partInfo.Params,group=partInfo.Group) h = system.addTransform(e,*partInfo.Params,group=partInfo.Group)
h = PointInfo(entity=h, params=partInfo.Params,vector=v) 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 partInfo.EntityMap[key] = h
return h if retAll else h.entity 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, h = NormalInfo(entity=nz,rot=rot,
params=partInfo.Params, p0=p0.entity, ln=ln) 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 partInfo.EntityMap[key] = h
return h if retAll else h.entity return h if retAll else h.entity
@ -194,7 +194,7 @@ def _l(solver,partInfo,subname,shape,retAll=False):
system.NameTag = nameTag system.NameTag = nameTag
h = system.addLineSegment(tp0,tp1,group=partInfo.Group) h = system.addLineSegment(tp0,tp1,group=partInfo.Group)
h = LineInfo(entity=h,p0=tp0,p1=tp1) 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 partInfo.EntityMap[key] = h
return h if retAll else h.entity return h if retAll else h.entity
@ -256,7 +256,7 @@ def _w(solver,partInfo,subname,shape,retAll=False):
system.NameTag = partInfo.PartName + '.' + key system.NameTag = partInfo.PartName + '.' + key
w = system.addWorkplane(p.entity,n.entity,group=partInfo.Group) w = system.addWorkplane(p.entity,n.entity,group=partInfo.Group)
h = PlaneInfo(entity=w,origin=p,normal=n) 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 return h if retAll else h.entity
def _wa(solver,partInfo,subname,shape,retAll=False): 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, e = system.addCircle(pln.origin.entity, pln.normal.entity,
system.addDistance(r), group=g) system.addDistance(r), group=g)
h = CircleInfo(entity=e,radius=r,p0=p0) 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: else:
system.NameTag = nameTag + '.c' system.NameTag = nameTag + '.c'
center = system.addPoint2d(pln.entity,solver.v0,solver.v0,group=g) 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 system.NameTag = nameTag
e = system.addArcOfCircle(pln.entity,center,*points,group=g) e = system.addArcOfCircle(pln.entity,center,*points,group=g)
h = ArcInfo(entity=e,p1=points[1],p0=points[0],params=params) 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 # exhaust all possible keys from a draft circle to save
# recomputation # recomputation
@ -352,7 +352,7 @@ def _c(solver,partInfo,subname,shape,requireArc=False,retAll=False):
h = system.addCircle( h = system.addCircle(
pln.origin.entity, pln.normal.entity, hr, group=g) pln.origin.entity, pln.normal.entity, hr, group=g)
h = CircleInfo(entity=h,radius=hr,p0=None) h = CircleInfo(entity=h,radius=hr,p0=None)
system.log('{}: {},{}'.format(key,h,g)) system.log('{}: {},{}'.format(nameTag,h,g))
partInfo.EntityMap[key] = h partInfo.EntityMap[key] = h
@ -461,6 +461,18 @@ class Constraint(ProxyType):
def isDisabled(mcs,obj): def isDisabled(mcs,obj):
return getattr(obj,mcs._disabled,False) 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 @classmethod
def check(mcs,tp,elements,checkCount=False): def check(mcs,tp,elements,checkCount=False):
mcs.getType(tp).check(elements,checkCount) mcs.getType(tp).check(elements,checkCount)
@ -552,6 +564,7 @@ _makeProp('Offset','App::PropertyDistance',getter=propGetValue)
_makeProp('OffsetX','App::PropertyDistance',getter=propGetValue) _makeProp('OffsetX','App::PropertyDistance',getter=propGetValue)
_makeProp('OffsetY','App::PropertyDistance',getter=propGetValue) _makeProp('OffsetY','App::PropertyDistance',getter=propGetValue)
_makeProp('Cascade','App::PropertyBool',internal=True) _makeProp('Cascade','App::PropertyBool',internal=True)
_makeProp('Multiply','App::PropertyBool',internal=True)
_makeProp('Angle','App::PropertyAngle',getter=propGetValue) _makeProp('Angle','App::PropertyAngle',getter=propGetValue)
_AngleProps = [ _AngleProps = [
@ -846,7 +859,7 @@ class BaseMulti(Base):
msg = cls._entityDef[0](None,info.Part,info.Subname,info.Shape) msg = cls._entityDef[0](None,info.Part,info.Subname,info.Shape)
if msg: if msg:
raise RuntimeError('Constraint "{}" requires all the element ' raise RuntimeError('Constraint "{}" requires all the element '
'to be of {}'.format(cls.getName())) 'to be of {}'.format(cls.getName(),msg))
return return
@classmethod @classmethod
@ -855,10 +868,45 @@ class BaseMulti(Base):
if not func: if not func:
logger.warn('{} no constraint func'.format(cstrName(obj))) logger.warn('{} no constraint func'.format(cstrName(obj)))
return 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() parts = set()
ref = None ref = None
elements = [] elements = []
props = cls.getPropertyValues(obj)
for e in obj.Proxy.getElements(): for e in obj.Proxy.getElements():
info = e.Proxy.getInfo() info = e.Proxy.getInfo()
@ -880,7 +928,6 @@ class BaseMulti(Base):
logger.warn('{} has no effective constraint'.format(cstrName(obj))) logger.warn('{} has no effective constraint'.format(cstrName(obj)))
return return
e0 = None e0 = None
ret = []
firstInfo = None firstInfo = None
for e in elements: for e in elements:
info = e.Proxy.getInfo() info = e.Proxy.getInfo()
@ -899,7 +946,6 @@ class BaseMulti(Base):
ret.append(h) ret.append(h)
return ret return ret
class BaseCascade(BaseMulti): class BaseCascade(BaseMulti):
@classmethod @classmethod
def prepare(cls,obj,solver): def prepare(cls,obj,solver):
@ -941,7 +987,7 @@ class BaseCascade(BaseMulti):
class PlaneCoincident(BaseCascade): class PlaneCoincident(BaseCascade):
_id = 35 _id = 35
_iconName = 'Assembly_ConstraintCoincidence.svg' _iconName = 'Assembly_ConstraintCoincidence.svg'
_props = ['Cascade','Offset','OffsetX','OffsetY'] + _AngleProps _props = ['Multiply','Cascade','Offset','OffsetX','OffsetY'] + _AngleProps
_tooltip = \ _tooltip = \
'Add a "{}" constraint to conincide planes of two or more parts.\n'\ 'Add a "{}" constraint to conincide planes of two or more parts.\n'\
'The planes are coincided at their centers with an optional distance.' 'The planes are coincided at their centers with an optional distance.'
@ -958,7 +1004,7 @@ class AxialAlignment(BaseMulti):
_id = 36 _id = 36
_entityDef = (_lna,) _entityDef = (_lna,)
_iconName = 'Assembly_ConstraintAxial.svg' _iconName = 'Assembly_ConstraintAxial.svg'
_props = _AngleProps _props = ['Multiply'] + _AngleProps
_tooltip = 'Add a "{}" constraint to align planes of two or more parts.\n'\ _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.' 'The planes are aligned at the direction of their surface normal axis.'

32
gui.py
View File

@ -258,7 +258,7 @@ class AsmCmdMove(AsmCmdBase):
@classmethod @classmethod
def checkActive(cls): def checkActive(cls):
cls._active = logger.catchTrace('',cls.canMove) cls._active = True if logger.catchTrace('',cls.canMove) else False
@classmethod @classmethod
def onClearSelection(cls): def onClearSelection(cls):
@ -628,3 +628,33 @@ class AsmCmdDown(AsmCmdUp):
@classmethod @classmethod
def Activated(cls): def Activated(cls):
cls.move(1) 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

View File

@ -90,7 +90,7 @@ def getElementShape(obj,tp=None,transform=False,noElementMap=True):
logger.trace('no sub object {}'.format(obj)) logger.trace('no sub object {}'.format(obj))
return return
if shape.isNull(): if shape.isNull():
if sobj.TypeId == 'App::Line': if sobj.isDerivedFrom('App::Line'):
if tp not in (None,Part.Shape,Part.Edge): if tp not in (None,Part.Shape,Part.Edge):
logger.trace('wrong type of shape {}'.format(obj)) logger.trace('wrong type of shape {}'.format(obj))
return return
@ -99,7 +99,7 @@ def getElementShape(obj,tp=None,transform=False,noElementMap=True):
FreeCAD.Vector(size,0,0)) FreeCAD.Vector(size,0,0))
shape.transformShape(mat,False,True) shape.transformShape(mat,False,True)
return shape return shape
elif sobj.TypeId == 'App::Plane': elif sobj.isDerivedFrom('App::Plane'):
if tp not in (None, Part.Shape, Part.Face): if tp not in (None, Part.Shape, Part.Face):
logger.trace('wrong type of shape {}'.format(obj)) logger.trace('wrong type of shape {}'.format(obj))
return return
@ -418,7 +418,7 @@ def getElementsAngle(o1,o2,pla1=None,pla2=None):
v2 = getElementDirection(o2,pla2) v2 = getElementDirection(o2,pla2)
return math.degrees(v1.getAngle(v2)) 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' 'return radius if it is closed, or a list of two endpoints'
edge = getElementShape(obj,Part.Edge) edge = getElementShape(obj,Part.Edge)
if not edge: if not edge:
@ -427,7 +427,7 @@ def getElementCircular(obj):
return return
c = edge.Curve c = edge.Curve
if hasattr( c, 'Radius' ): if hasattr( c, 'Radius' ):
if edge.Closed: if radius or edge.Closed:
return c.Radius return c.Radius
elif isLine(edge.Curve): elif isLine(edge.Curve):
return return
@ -437,7 +437,7 @@ def getElementCircular(obj):
arc = BSpline.toBiArcs(10**-6)[0] arc = BSpline.toBiArcs(10**-6)[0]
except Exception: #FreeCAD exception thrown () except Exception: #FreeCAD exception thrown ()
return return
if edge.Closed: if radius or edge.Closed:
return arc[0].Radius return arc[0].Radius
return [v.Point for v in edge.Vertexes] return [v.Point for v in edge.Vertexes]