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.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)

View File

@ -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.'

32
gui.py
View File

@ -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

View File

@ -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]