433 lines
16 KiB
Python
433 lines
16 KiB
Python
import os
|
|
import FreeCAD
|
|
try:
|
|
from six import with_metaclass
|
|
except ImportError:
|
|
from .deps import with_metaclass
|
|
from .constraint import cstrName, PlaneInfo, NormalInfo
|
|
from .utils import getIcon, syslogger as logger, objName, project2D, getNormal
|
|
from .proxy import ProxyType, PropertyInfo
|
|
|
|
class System(ProxyType):
|
|
'solver system meta class'
|
|
|
|
_typeID = '_SolverType'
|
|
_typeEnum = 'SolverType'
|
|
_propGroup = 'Solver'
|
|
_iconName = 'Assembly_Assembly_Tree.svg'
|
|
|
|
@classmethod
|
|
def setDefaultTypeID(mcs,obj,name=None):
|
|
if not name:
|
|
info = mcs.getInfo()
|
|
idx = 1 if len(info.TypeNames)>1 else 0
|
|
name = info.TypeNames[idx]
|
|
super(System,mcs).setDefaultTypeID(obj,name)
|
|
|
|
@classmethod
|
|
def getIcon(mcs,obj):
|
|
func = getattr(mcs.getProxy(obj),'getIcon',None)
|
|
if func:
|
|
icon = func(obj)
|
|
if icon:
|
|
return icon
|
|
return getIcon(mcs,mcs.isDisabled(obj))
|
|
|
|
@classmethod
|
|
def isDisabled(mcs,obj):
|
|
proxy = mcs.getProxy(obj)
|
|
return not proxy or proxy.isDisabled(obj)
|
|
|
|
@classmethod
|
|
def isTouched(mcs,obj):
|
|
proxy = mcs.getProxy(obj)
|
|
return proxy and proxy.isTouched(obj)
|
|
|
|
@classmethod
|
|
def touch(mcs,obj,touched=True):
|
|
proxy = mcs.getProxy(obj)
|
|
if proxy:
|
|
proxy.touch(obj,touched)
|
|
|
|
@classmethod
|
|
def onChanged(mcs,obj,prop):
|
|
proxy = mcs.getProxy(obj)
|
|
if proxy:
|
|
proxy.onChanged(obj,prop)
|
|
if super(System,mcs).onChanged(obj,prop):
|
|
obj.Proxy.onSolverChanged()
|
|
|
|
@classmethod
|
|
def getSystem(mcs,obj):
|
|
proxy = mcs.getProxy(obj)
|
|
if proxy:
|
|
system = proxy.getSystem(obj)
|
|
if isinstance(system,SystemExtension):
|
|
system.relax = obj.AutoRelax
|
|
return system
|
|
|
|
@classmethod
|
|
def isConstraintSupported(mcs,obj,name):
|
|
if name == 'Locked':
|
|
return True
|
|
proxy = mcs.getProxy(obj)
|
|
if proxy:
|
|
return proxy.isConstraintSupported(name)
|
|
|
|
def _makePropInfo(name,tp,doc='',default=None):
|
|
PropertyInfo(System,name,tp,doc,group='Solver',default=default)
|
|
|
|
_makePropInfo('Verbose','App::PropertyBool')
|
|
_makePropInfo('AutoRelax','App::PropertyBool',default=True)
|
|
|
|
class SystemBase(with_metaclass(System, object)):
|
|
_id = 0
|
|
_props = ['Verbose','AutoRelax']
|
|
|
|
def __init__(self,obj):
|
|
self._touched = True
|
|
self.verbose = obj.Verbose
|
|
self.log = logger.info if self.verbose else logger.debug
|
|
super(SystemBase,self).__init__()
|
|
|
|
@classmethod
|
|
def getPropertyInfoList(cls):
|
|
return cls._props
|
|
|
|
@classmethod
|
|
def getName(cls):
|
|
return 'None'
|
|
|
|
def isConstraintSupported(self,_cstrName):
|
|
return True
|
|
|
|
def isDisabled(self,_obj):
|
|
return True
|
|
|
|
def isTouched(self,_obj):
|
|
return getattr(self,'_touched',True)
|
|
|
|
def touch(self,_obj,touched=True):
|
|
self._touched = touched
|
|
|
|
def onChanged(self,obj,prop):
|
|
if prop == 'Verbose':
|
|
self.verbose = obj.Verbose
|
|
self.log = logger.info if obj.Verbose else logger.debug
|
|
|
|
def _cstrKey(cstrType, firstPart, secondPart):
|
|
if firstPart > secondPart:
|
|
return (cstrType, secondPart, firstPart)
|
|
else:
|
|
return (cstrType, firstPart, secondPart)
|
|
|
|
# For skipping invalid constraints
|
|
_DummyCstrList = [None] * 6
|
|
|
|
class SystemExtension(object):
|
|
def __init__(self):
|
|
super(SystemExtension,self).__init__()
|
|
self.NameTag = ''
|
|
self.sketchPlane = None
|
|
self.cstrObj = None
|
|
self.firstInfo = None
|
|
self.secondInfo = None
|
|
self.relax = False
|
|
self.coincidences = {}
|
|
self.cstrMap = {}
|
|
self.elementCstrMap = {}
|
|
self.elementMap = {}
|
|
self.firstElement = None
|
|
self.secondElement = None
|
|
|
|
def checkRedundancy(self,obj,firstInfo,secondInfo,firstElement,secondElement):
|
|
self.cstrObj,self.firstInfo,self.secondInfo=obj,firstInfo,secondInfo
|
|
self.firstElement = firstElement
|
|
self.secondElement = secondElement
|
|
|
|
def addSketchPlane(self,*args,**kargs):
|
|
_ = kargs
|
|
self.sketchPlane = args[0] if args else None
|
|
return self.sketchPlane
|
|
|
|
def setOrientation(self,h,lockAngle,yaw,pitch,roll,n1,n2,group):
|
|
if not lockAngle:
|
|
h.append(self.addParallel(n1.entity,n2.entity,group=group))
|
|
return h
|
|
if not yaw and not pitch and not roll:
|
|
n = n2.entity
|
|
else:
|
|
rot = n2.rot.multiply(FreeCAD.Rotation(yaw,pitch,roll))
|
|
e = self.addNormal3dV(*getNormal(rot))
|
|
n = self.addTransform(e,*n2.params,group=group)
|
|
h.append(self.addSameOrientation(n1.entity,n,group=group))
|
|
return h
|
|
|
|
def reportRedundancy(self,firstPart=None,secondPart=None,count=0,limit=0,implicit=False):
|
|
msg = '{} between {} and {}'.format(cstrName(self.cstrObj),
|
|
firstPart if firstPart else self.firstInfo.PartName,
|
|
secondPart if secondPart else self.secondInfo.PartName)
|
|
if implicit:
|
|
logger.msg('redundant implicit constraint {}, {}', msg, count, frame=1)
|
|
elif count > limit:
|
|
logger.warn('skip redundant {}, {}', msg, count, frame=1)
|
|
else:
|
|
logger.msg('auto relax {}, {}', msg, count, frame=1)
|
|
|
|
def _populateConstraintMap(
|
|
self,cstrType,firstElement,secondElement,increment,limit,item,implicit):
|
|
|
|
firstPart = self.elementMap[firstElement]
|
|
secondPart = self.elementMap[secondElement]
|
|
if firstPart == secondPart:
|
|
return _DummyCstrList
|
|
|
|
# A constraint may contain elements belong to more than two parts. For
|
|
# exmaple, for a constraint with elements from part A, B, C, we'll
|
|
# expand it into two constraints for parts AB and AC. However, we must
|
|
# also count the implicit constraint between B and C.
|
|
#
|
|
# self.cstrMap is a map for counting constraints of the same type
|
|
# between pairs of parts. The count is used for checking redundancy and
|
|
# auto relaxing. The map is keyed using
|
|
#
|
|
# tuple(cstrType, firstPartName, secondPartName)
|
|
#
|
|
# and the value is a list. The item of this list is constraint defined
|
|
# (e.g. PlaineAilgnment stores a plane entity as item for auto
|
|
# relaxing) , the length of this list is use as the constraint count to
|
|
# be used later to decide how to auto relax the constraint.
|
|
#
|
|
# See the following link for difficulties on auto relaxing with implicit
|
|
# constraints. Right now there is no search performed. So the auto relax
|
|
# may fail. And the user is required to manually reorder constraints and
|
|
# the elements within to help the solver.
|
|
#
|
|
# https://github.com/realthunder/FreeCAD_assembly3/issues/403#issuecomment-757400349
|
|
|
|
key = _cstrKey(cstrType,firstPart,secondPart)
|
|
cstrs = self.cstrMap.setdefault(key, [])
|
|
cstrs += [item]*increment
|
|
count = len(cstrs)
|
|
if increment and count>=limit:
|
|
self.reportRedundancy(firstPart, secondPart, count, limit, implicit)
|
|
return cstrs
|
|
|
|
def _countConstraints(self,increment,limit,cstrType,item=None):
|
|
first, second = self.firstInfo, self.secondInfo
|
|
if not first or not second:
|
|
return []
|
|
|
|
firstElement, secondElement = self.firstElement, self.secondElement
|
|
|
|
if firstElement == secondElement:
|
|
return _DummyCstrList
|
|
|
|
self.elementMap[firstElement] = first.PartName
|
|
self.elementMap[secondElement] = second.PartName
|
|
|
|
# When counting implicit constraints (see comments in
|
|
# _populateConstraintMap() above), we must also make sure to count them
|
|
# if and only if they are originated from the same element, i.e. both
|
|
# AB and AC involving the same element of A. This will be ture if the
|
|
# those constraints are expanded by us, but may not be so if the user
|
|
# created them.
|
|
#
|
|
# self.elementCstrMap is a map keyed using tuple(cstrType, elementName),
|
|
# with value of a set of all element names that is involved with the the
|
|
# same type of constraint. This set is shared by all element entries in
|
|
# the map.
|
|
|
|
firstSet = self.elementCstrMap.setdefault((cstrType, firstElement), set())
|
|
if not firstSet:
|
|
firstSet.add(firstElement)
|
|
secondSet = self.elementCstrMap.setdefault((cstrType, secondElement),firstSet)
|
|
|
|
res = _DummyCstrList
|
|
|
|
if firstSet is not secondSet:
|
|
# If the secondSet is different, we shall merge them, and count the
|
|
# implicit constraints between the elements of first and second set.
|
|
for element in secondSet:
|
|
self.elementCstrMap[(cstrType, element)] = firstSet
|
|
is_second = element == secondElement
|
|
for e in firstSet:
|
|
implicit = not is_second or e != firstElement
|
|
cstrs = self._populateConstraintMap(
|
|
cstrType,e,element,increment,limit,item,implicit)
|
|
if not implicit:
|
|
# save the result (i.e. the explicit constraint pair of
|
|
# the give first and second element) for return
|
|
res = cstrs
|
|
firstSet |= secondSet
|
|
elif secondElement not in firstSet:
|
|
# Here means the entry of the secondElement is newly created, count
|
|
# the implicit constraints between all elements in the set to the
|
|
# secondElement.
|
|
for e in firstSet:
|
|
implicit = e != firstElement
|
|
cstrs = self._populateConstraintMap(
|
|
cstrType,e,secondElement,increment,limit,item,implicit)
|
|
if not implicit:
|
|
res = cstrs
|
|
firstSet.add(secondElement)
|
|
|
|
if res is _DummyCstrList:
|
|
self.reportRedundancy(count=len(res), limit=limit)
|
|
return res
|
|
|
|
def countConstraints(self,increment,limit,name):
|
|
count = len(self._countConstraints(increment,limit,name))
|
|
if count>limit:
|
|
return -1
|
|
return count
|
|
|
|
def addPlaneCoincident(
|
|
self, d, dx, dy, lockAngle, yaw, pitch, roll, pln1, pln2, group=0):
|
|
if not group:
|
|
group = self.GroupHandle
|
|
h = []
|
|
|
|
count=self.countConstraints(2 if lockAngle else 1,2,'Coincident')
|
|
if count < 0:
|
|
return
|
|
|
|
if count == 1:
|
|
self.coincidences[(self.firstInfo.Part, self.secondInfo.Part)] = pln1
|
|
self.coincidences[(self.secondInfo.Part, self.firstInfo.Part)] = pln2
|
|
|
|
if d or dx or dy:
|
|
dx,dy,d = pln2.normal.rot.multVec(FreeCAD.Vector(dx,dy,d))
|
|
v = pln2.origin.vector+FreeCAD.Vector(dx,dy,d)
|
|
e = self.addTransform(
|
|
self.addPoint3dV(*v),*pln2.origin.params,group=group)
|
|
else:
|
|
v = pln2.origin.vector
|
|
e = pln2.origin.entity
|
|
|
|
if not lockAngle and count==2:
|
|
# if there is already some other plane coincident constraint set for
|
|
# this pair of parts, we reduce this second constraint to a 2D
|
|
# PointOnLine. The line is formed by the first part's two elements
|
|
# in the previous and the current constraint. The point is taken
|
|
# from the element of the second part of the current constraint.
|
|
# The projection plane is taken from the element of the first part
|
|
# of the current constraint.
|
|
#
|
|
# This 2D PointOnLine effectively reduce the second PlaneCoincidence
|
|
# constraining DOF down to 1.
|
|
prev = self.coincidences.get(
|
|
(self.firstInfo.Part, self.secondInfo.Part))
|
|
ln = self.addLineSegment(prev.origin.entity,
|
|
pln1.origin.entity, group=self.firstInfo.Group)
|
|
h.append(self.addPointOnLine(
|
|
pln2.origin.entity, ln, pln1.entity, group=group))
|
|
return h
|
|
|
|
h.append(self.addPointsCoincident(pln1.origin.entity, e, group=group))
|
|
|
|
return self.setOrientation(h, lockAngle, yaw, pitch, roll,
|
|
pln1.normal, pln2.normal, group)
|
|
|
|
def addAttachment(self, pln1, pln2, group=0):
|
|
return self.addPlaneCoincident(0,0,0,True,0,0,0, pln1, pln2, group)
|
|
|
|
def addPlaneAlignment(self,d,lockAngle,yaw,pitch,roll,pln1,pln2,group=0):
|
|
if not group:
|
|
group = self.GroupHandle
|
|
h = []
|
|
if self.relax:
|
|
dof = 2 if lockAngle else 1
|
|
cstrs = self._countConstraints(dof,3,'Alignment',item=pln1.entity)
|
|
count = len(cstrs)
|
|
if count > 3:
|
|
return
|
|
else:
|
|
count = 0
|
|
|
|
if d:
|
|
h.append(self.addPointPlaneDistance(
|
|
d, pln2.origin.entity, pln1.entity, group=group))
|
|
else:
|
|
h.append(self.addPointInPlane(
|
|
pln2.origin.entity, pln1.entity,group=group))
|
|
if count<=2:
|
|
n1,n2 = pln1.normal,pln2.normal
|
|
if count==2 and not lockAngle:
|
|
self.reportRedundancy(count=count, limit=count)
|
|
h.append(self.addParallel(n2.entity,n1.entity,cstrs[0],group))
|
|
else:
|
|
self.setOrientation(h,lockAngle,yaw,pitch,roll,n1,n2,group)
|
|
return h
|
|
|
|
def addAxialAlignment(self,lockAngle,yaw,pitch,roll,ln1,ln2,group=0):
|
|
if not group:
|
|
group = self.GroupHandle
|
|
h = []
|
|
if not isinstance(ln1,NormalInfo):
|
|
if not isinstance(ln2,NormalInfo):
|
|
lockAngle = False
|
|
else:
|
|
ln1,ln2 = ln2,ln1
|
|
|
|
count = self.countConstraints(2 if lockAngle else 1,2,'Axial')
|
|
if count < 0:
|
|
return
|
|
relax = count==2 and not lockAngle
|
|
if isinstance(ln2,NormalInfo):
|
|
ln = ln2.ln
|
|
if not relax:
|
|
h = self.setOrientation(
|
|
h,lockAngle,yaw,pitch,roll,ln1,ln2,group)
|
|
else:
|
|
ln = ln2.entity
|
|
if not relax:
|
|
h.append(self.addParallel(ln1.entity,ln,group=group))
|
|
h.append(self.addPointOnLine(ln1.p0,ln,group=group))
|
|
return h
|
|
|
|
def addMultiParallel(self,lockAngle,yaw,pitch,raw,e1,e2,group=0):
|
|
if not group:
|
|
group = self.GroupHandle
|
|
h = []
|
|
isPlane = isinstance(e1,PlaneInfo),isinstance(e2,PlaneInfo)
|
|
if all(isPlane):
|
|
return self.setOrientation(h, lockAngle, yaw, pitch, raw,
|
|
e1.normal, e2.normal, group);
|
|
if not any(isPlane):
|
|
h.append(self.addParallel(e1, e2, group=group))
|
|
elif isPlane[0]:
|
|
h.append(self.addPerpendicular(e1.normal.entity, e2, group=group))
|
|
else:
|
|
h.append(self.addPerpendicular(e1, e2.normal.entity, group=group))
|
|
return h
|
|
|
|
def addColinear(self,l1,l2,wrkpln=0,group=0):
|
|
h = []
|
|
if isinstance(l1,NormalInfo):
|
|
pt = l1.p0
|
|
l1 = l1.ln
|
|
else:
|
|
pt = l1.p0
|
|
l1 = l1.entity
|
|
if isinstance(l2,NormalInfo):
|
|
l2 = l2.ln
|
|
else:
|
|
l2 = l2.entity
|
|
h.append(self.addParallel(l1,l2,wrkpln=wrkpln,group=group))
|
|
h.append(self.addPointOnLine(pt,l2,wrkpln=wrkpln,group=group))
|
|
return h
|
|
|
|
def addPlacement(self,pla,group=0):
|
|
q = pla.Rotation.Q
|
|
base = pla.Base
|
|
nameTagSave = self.NameTag
|
|
nameTag = nameTagSave+'.' if nameTagSave else 'pla.'
|
|
ret = []
|
|
for n,v in (('x',base.x),('y',base.y),('z',base.z),
|
|
('qw',q[3]),('qx',q[0]),('qy',q[1]),('qz',q[2])):
|
|
self.NameTag = nameTag+n
|
|
ret.append(self.addParamV(v,group))
|
|
self.NameTag = nameTagSave
|
|
return ret
|