FreeCAD_assembly3/constraint.py
Zheng, Lei 9402201a5e More constraints working, and a few new ones
Working constraints:

PointPlaneDistance
PointInPlane
Angle
Perpendicular
Parallel

New constraints, which are composite of slvs native constraints:

PlaneCoincident
PlaneAlignment
AxialAlignment
MultiParallel
2017-10-31 04:06:36 +08:00

780 lines
23 KiB
Python

from collections import namedtuple
from PySide.QtCore import Qt
from PySide.QtGui import QIcon, QPainter, QPixmap
import FreeCAD, FreeCADGui
import asm3.utils as utils
import asm3.slvs as slvs
from asm3.utils import logger, objName
import os
iconPath = os.path.join(utils.iconPath,'constraints')
pixmapDisabled = QPixmap(os.path.join(
iconPath,'Assembly_ConstraintDisabled.svg'))
iconSize = (16,16)
def cstrName(obj):
return '{}<{}>'.format(objName(obj),obj.Type)
PropertyInfo = namedtuple('AsmPropertyInfo',
('Name','Type','Group','Doc','Enum','Getter'))
_propInfo = {}
def _propGet(obj,prop):
return getattr(obj,prop)
def _propGetValue(obj,prop):
return getattr(getattr(obj,prop),'Value')
def _makePropInfo(name,tp,doc='',enum=None,getter=_propGet,group='Constraint'):
_propInfo[name] = PropertyInfo(name,tp,group,doc,enum,getter)
_makePropInfo('Distance','App::PropertyDistance',getter=_propGetValue)
_makePropInfo('Offset','App::PropertyDistance',getter=_propGetValue)
_makePropInfo('Cascade','App::PropertyBool')
_makePropInfo('Angle','App::PropertyAngle',getter=_propGetValue)
_makePropInfo('Ratio','App::PropertyFloat')
_makePropInfo('Difference','App::PropertyFloat')
_makePropInfo('Diameter','App::PropertyFloat')
_makePropInfo('Radius','App::PropertyFloat')
_makePropInfo('Supplement','App::PropertyBool',
'If True, then the second angle is calculated as 180-angle')
_makePropInfo('AtEnd','App::PropertyBool',
'If True, then tangent at the end point, or else at the start point')
_ordinal = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th' ]
Types = []
TypeMap = {}
TypeNameMap = {}
class ConstraintType(type):
def __init__(cls, name, bases, attrs):
super(ConstraintType,cls).__init__(name,bases,attrs)
if cls._id >= 0:
if cls._id in TypeMap:
raise RuntimeError(
'Duplicate constriant type id {}'.format(cls._id))
if not cls.slvsFunc():
return
if cls._props:
for i,prop in enumerate(cls._props):
try:
cls._props[i] = _propInfo[prop]
except AttributeError:
raise RuntimeError('Unknonw property "{}" in '
'constraint type "{}"'.format(prop,cls.getName()))
TypeMap[cls._id] = cls
TypeNameMap[cls.getName()] = cls
cls._idx = len(Types)
logger.debug('register constraint "{}":{},{}'.format(
cls.getName(),cls._id,cls._idx))
Types.append(cls)
# PartName: text name of the part
# Placement: the original placement of the part
# Params: 7 parameters that defines the transformation of this part
# RParams: 7 parameters that defines the rotation transformation of this part
# Workplane: a tuple of three entity handles, that is the workplane, the origin
# point, and the normal. The workplane, defined by the origin and
# norml, is essentially the XY reference plane of the part.
# EntityMap: string -> entity handle map, for caching
# Group: transforming entity group handle
# X: a point entity (1,0,0) rotated by this part's placement
# Y: a point entity (0,1,0) rotated by this part's placement
# Z: a point entity (0,0,1) rotated by this part's placement
PartInfo = namedtuple('SolverPartInfo',
('PartName','Placement','Params','RParams','Workplane','EntityMap',
'Group', 'X','Y','Z'))
def _p(solver,partInfo,key,shape):
'return a slvs handle of a transformed point derived from "shape"'
if not solver:
if utils.hasCenter(shape):
return
return 'a vertex or circular edge/face'
key += '.p'
h = partInfo.EntityMap.get(key,None)
if h:
logger.debug('cache {}: {}'.format(key,h))
else:
v = utils.getElementPos(shape)
system = solver.system
e = system.addPoint3dV(*v)
h = system.addTransform(e,*partInfo.Params,group=partInfo.Group)
logger.debug('{}: {},{}, {}, {}'.format(key,h,partInfo.Group,e,v))
partInfo.EntityMap[key] = h
return h
def _n(solver,partInfo,key,shape,retAll=False):
'return a slvs handle of a transformed normal quaterion derived from shape'
if not solver:
if utils.isPlanar(shape):
return
return 'an edge or face with a surface normal'
key += '.n'
h = partInfo.EntityMap.get(key,None)
if h:
logger.debug('cache {}: {}'.format(key,h))
else:
system = solver.system
params = [ system.addParamV(n) for n in utils.getElementNormal(shape) ]
e = system.addNormal3d(*params)
h = system.addTransform(e,*partInfo.Params,group=partInfo.Group)
h = [h,e,params]
logger.debug('{}: {},{}'.format(key,h,partInfo.Group))
partInfo.EntityMap[key] = h
return h if retAll else h[0]
def _l(solver,partInfo,key,shape,retAll=False):
'return a pair of slvs handle of the end points of an edge in "shape"'
if not solver:
if utils.isLinearEdge(shape):
return
return 'a linear edge'
key += '.l'
h = partInfo.EntityMap.get(key,None)
if h:
logger.debug('cache {}: {}'.format(key,h))
else:
system = solver.system
v = shape.Edges[0].Vertexes
p1 = system.addPoint3dV(*v[0].Point)
p2 = system.addPoint3dV(*v[-1].Point)
h = system.addLineSegment(p1,p2,group=partInfo.Group)
h = (h,p1,p2)
logger.debug('{}: {},{}'.format(key,h,partInfo.Group))
partInfo.EntityMap[key] = h
return h if retAll else h[0]
def _ln(solver,partInfo,key,shape,retAll=False):
'return a slvs handle for either a line or a normal depends on the shape'
if not solver:
if utils.isLinearEdge(shape) or utils.isPlanar(shape):
return
return 'a linear edge or edge/face with planar surface'
if utils.isLinearEdge(shape):
return _l(solver,partInfo,key,shape,retAll)
return _n(solver,partInfo,key,shape)
def _w(solver,partInfo,key,shape,retAll=False):
'return a slvs handle of a transformed plane/workplane from "shape"'
if not solver:
if utils.isPlanar(shape):
return
return 'an edge/face with a planar surface'
key2 = key+'.w'
h = partInfo.EntityMap.get(key2,None)
if h:
logger.debug('cache {}: {}'.format(key,h))
else:
p = _p(solver,partInfo,key,shape)
n = _n(solver,partInfo,key,shape)
h = solver.system.addWorkplane(p,n,group=partInfo.Group)
h = (h,p,n)
logger.debug('{}: {},{}'.format(key,h,partInfo.Group))
partInfo.EntityMap[key2] = h
return h if retAll else h[0]
def _wa(solver,partInfo,key,shape):
return _w(solver,partInfo,key,shape,True)
def _c(solver,partInfo,key,shape,requireArc=False):
'return a slvs handle of a transformed circle/arc derived from "shape"'
if not solver:
r = utils.getElementCircular(shape)
if not r or (requireArc and not isinstance(r,list,tuple)):
return
return 'an cicular arc edge' if requireArc else 'a circular edge'
key2 = key+'.c'
h = partInfo.EntityMap.get(key2,None)
if h:
logger.debug('cache {}: {}'.format(key,h))
else:
h = _w(solver,partInfo,key,shape,True)
r = utils.getElementCircular(shape)
if not r:
raise RuntimeError('shape is not cicular')
if isinstance(r,(list,tuple)):
l = _l(solver,partInfo,key,shape,True)
h += l[1:]
h = solver.system.addArcOfCircleV(*h,group=partInfo.Group)
elif requireArc:
raise RuntimeError('shape is not an arc')
else:
h = h[1:]
h.append(solver.addDistanceV(r))
h = solver.system.addCircle(*h,group=partInfo.Group)
logger.debug('{}: {},{} {}'.format(key,h,partInfo.Group,r))
partInfo.EntityMap[key2] = h
return h
def _a(solver,partInfo,key,shape):
return _c(solver,partInfo,key,shape,True)
class Base:
__metaclass__ = ConstraintType
_id = -1
_entityDef = []
_workplane = False
_props = []
_func = None
_icon = None
_iconDisabled = None
_iconName = 'Assembly_ConstraintGeneral.svg'
def __init__(self,obj):
if obj._Type != self._id:
if self._id < 0:
raise RuntimeError('invalid constraint type {} id: '
'{}'.format(self.__class__,self._id))
obj._Type = self._id
props = obj.PropertiesList
for prop in self.__class__._props:
if prop.Name not in props:
obj.addProperty(prop.Type,prop.Name,prop.Group,prop.Doc)
if prop.Enum:
setattr(obj,prop.Name,prop.Enum)
else:
obj.setPropertyStatus(prop.Name,'-Hidden')
@classmethod
def getName(cls):
return cls.__name__
@classmethod
def slvsFunc(cls):
try:
if not cls._func:
cls._func = getattr(slvs.System,'add'+cls.getName())
return cls._func
except AttributeError:
logger.error('Invalid slvs constraint "{}"'.format(cls.getName()))
@classmethod
def getEntityDef(cls,group,checkCount,obj=None):
entities = cls._entityDef
if len(group) != len(entities):
if not checkCount and len(group)<len(entities):
return entities[:len(group)]
if cls._workplane and len(group)==len(entities)+1:
entities = list(entities)
entities.append(_w)
else:
if not obj:
name = cls.getName()
else:
name += cstrName(obj)
raise RuntimeError('Constraint {} has wrong number of '
'elements {}, expecting {}'.format(
name,len(group),len(entities)))
return entities
@classmethod
def check(cls,group):
entities = cls.getEntityDef(group,False)
for i,e in enumerate(entities):
o = group[i]
msg = e(None,None,None,o)
if not msg:
continue
if i == len(cls._entityDef):
raise RuntimeError('Constraint {} requires the optional {} '
'element to be a planar face for defining a '
'workplane'.format(cls.getName(), _ordinal[i], msg))
raise RuntimeError('Constraint {} requires the {} element to be'
' {}'.format(cls.getName(), _ordinal[i], msg))
@classmethod
def getIcon(cls,obj):
if not cls._icon:
cls._icon = QIcon(os.path.join(iconPath,cls._iconName))
if not obj.Disabled:
return cls._icon
if not cls._iconDisabled:
pixmap = cls._icon.pixmap(*iconSize,mode=QIcon.Disabled)
icon = QIcon(pixmapDisabled)
icon.paint(QPainter(pixmap),
0,0,iconSize[0],iconSize[1],Qt.AlignCenter)
cls._iconDisabled = QIcon(pixmap)
return cls._iconDisabled
@classmethod
def detach(cls,obj):
logger.debug('detaching {}'.format(cstrName(obj)))
obj.Proxy._cstr = None
for prop in cls._props:
# obj.setPropertyStatus(prop.Name,'Hidden')
obj.removeProperty(prop.Name)
def onChanged(self,obj,prop):
pass
@classmethod
def getEntities(cls,obj,solver):
'''maps fcad element shape to slvs entities'''
ret = []
for prop in cls._props:
ret.append(prop.Getter(obj,prop.Name))
elements = obj.Proxy.getElements()
entities = cls.getEntityDef(elements,True,obj)
for e,o in zip(entities,elements):
info = o.Proxy.getInfo()
partInfo = solver.getPartInfo(info)
ret.append(e(solver,partInfo,info.Subname,info.Shape))
logger.debug('{} entities: {}'.format(cstrName(obj),ret))
return ret
@classmethod
def prepare(cls,obj,solver):
e = cls.getEntities(obj,solver)
h = cls._func(solver.system,*e,group=solver.group)
logger.debug('{} constraint: {}'.format(cstrName(obj),h))
class Locked(Base):
_id = 0
_func = True
_iconName = 'Assembly_ConstraintLock.svg'
@classmethod
def prepare(cls,obj,solver):
for e in obj.Proxy.getElements():
solver.addFixedPart(e.Proxy.getInfo())
@classmethod
def check(cls,_group):
pass
class BaseMulti(Base):
_id = -1
_func = True
_entityDef = [_wa]
@classmethod
def check(cls,group):
if len(group)<2:
raise RuntimeError('Constraint {} requires at least two '
'elements'.format(cls.getName()))
for o in group:
msg = cls._entityDef[0](None,None,None,o)
if msg:
raise RuntimeError('Constraint {} requires all the element '
'to be of {}'.format(cls.getName()))
return
@classmethod
def prepare(cls,obj,solver):
parts = set()
ref = None
elements = []
for e in obj.Proxy.getElements():
info = e.Proxy.getInfo()
if info.Part in parts:
logger.warn('{} skip duplicate parts {}'.format(
cstrName(obj),info.PartName))
continue
parts.add(info.Part)
if solver.isFixedPart(info):
if ref:
logger.warn('{} skip more than one fixed part {}'.format(
cstrName(obj),info.PartName))
continue
ref = info
elements.insert(0,e)
else:
elements.append(e)
if len(elements)<=1:
logger.warn('{} has no effective constraint'.format(cstrName(obj)))
return
e0 = None
for e in elements:
info = e.Proxy.getInfo()
partInfo = solver.getPartInfo(info)
if not e0:
e0 = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape)
else:
e = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape)
cls.prepareElements(obj,solver,e0,e)
class BaseCascade(BaseMulti):
@classmethod
def prepare(cls,obj,solver):
if not getattr(obj,'Cascade',True):
super(BaseCascade,cls).prepare(obj,solver)
return
prev = None
count = 0
for e in obj.Proxy.getElements():
info = e.Proxy.getInfo()
if not prev or prev.Part==info.Part:
prev = info
continue
count += 1
prevInfo = solver.getPartInfo(prev)
e1 = cls._entityDef[0](solver,prevInfo,prev.Subname,prev.Shape)
partInfo = solver.getPartInfo(info)
e2 = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape)
prev = info
if solver.isFixedPart(info):
e2,e1 = e1,e2
cls.prepareElements(obj,solver,e1,e2)
if not count:
logger.warn('{} has no effective constraint'.format(cstrName(obj)))
class PlaneCoincident(BaseCascade):
_id = 35
_iconName = 'Assembly_ConstraintCoincidence.svg'
_props = ["Offset", 'Cascade']
@classmethod
def prepareElements(cls,obj,solver,e1,e2):
system = solver.system
d = abs(obj.Offset.Value)
_,p1,n1 = e1
w2,p2,n2 = e2
if d>0.0:
h = system.addPointPlaneDistance(d,p1,w2,group=solver.group)
logger.debug('{}: point plane distance {},{},{}'.format(
cstrName(obj),h,p1,w2,d))
h = system.addPointsCoincident(p1,p2,w2,group=solver.group)
logger.debug('{}: points conincident {},{},{}'.format(
cstrName(obj),h,p1,p2,w2))
else:
h = system.addPointsCoincident(p1,p2,group=solver.group)
logger.debug('{}: points conincident {},{},{}'.format(
cstrName(obj),h,p1,p2))
h = system.addParallel(n1,n2,group=solver.group)
logger.debug('{}: parallel {},{},{}'.format(cstrName(obj),h,n1,n2))
class PlaneAlignment(BaseCascade):
_id = 37
_iconName = 'Assembly_ConstraintAlignment.svg'
_props = ["Offset", 'Cascade']
@classmethod
def prepareElements(cls,obj,solver,e1,e2):
system = solver.system
d = abs(obj.Offset.Value)
_,p1,n1 = e1
w2,_,n2 = e2
if d>0.0:
h = system.addPointPlaneDistance(d,p1,w2,group=solver.group)
logger.debug('{}: point plane distance {},{},{}'.format(
cstrName(obj),h,p1,w2,d))
else:
h = system.addPointInPlane(p1,w2,group=solver.group)
logger.debug('{}: point in plane {},{}'.format(
cstrName(obj),h,p1,w2))
h = system.addParallel(n1,n2,group=solver.group)
logger.debug('{}: parallel {},{},{}'.format(cstrName(obj),h,n1,n2))
class AxialAlignment(BaseMulti):
_id = 36
_iconName = 'Assembly_ConstraintAxial.svg'
@classmethod
def prepareElements(cls,obj,solver,e1,e2):
system = solver.system
_,p1,n1 = e1
w2,p2,n2 = e2
h = system.addPointsCoincident(p1,p2,w2,group=solver.group)
logger.debug('{}: points coincident {},{},{},{}'.format(
cstrName(obj),h,p1,p2,w2))
h = system.addParallel(n1,n2,group=solver.group)
logger.debug('{}: parallel {},{},{}'.format(cstrName(obj),h,n1,n2))
class SameOrientation(BaseMulti):
_id = 2
_entityDef = [_n]
_iconName = 'Assembly_ConstraintOrientation.svg'
@classmethod
def prepareElements(cls,obj,solver,n1,n2):
h = solver.system.addSameOrientation(n1,n2,group=solver.group)
logger.debug('{}: {} {},{},{}'.format(
cstrName(obj),cls.getName(),h,n1,n2))
class Angle(Base):
_id = 27
_entityDef = (_ln,_ln)
_workplane = True
_props = ["Angle","Supplement"]
_iconName = 'Assembly_ConstraintAngle.svg'
class Perpendicular(Base):
_id = 28
_entityDef = (_ln,_ln)
_workplane = True
_iconName = 'Assembly_ConstraintPerpendicular.svg'
class Parallel(Base):
_id = 29
_entityDef = (_ln,_ln)
_workplane = True
_iconName = 'Assembly_ConstraintParallel.svg'
class MultiParallel(BaseMulti):
_id = 291
_entityDef = [_ln]
_iconName = 'Assembly_ConstraintMultiParallel.svg'
@classmethod
def prepareElements(cls,obj,solver,e1,e2):
h = solver.system.addParallel(e1,e2,group=solver.group)
logger.debug('{}: {} {},{},{}'.format(
cstrName(obj),cls.getName(),h,e1,e2))
class PointsCoincident(Base):
_id = 1
_entityDef = (_p,_p)
_workplane = True
class PointInPlane(Base):
_id = 3
_entityDef = (_p,_w)
class PointOnLine(Base):
_id = 4
_entityDef = (_p,_l)
_workplane = True
class PointsDistance(Base):
_id = 5
_entityDef = (_p,_p)
_workplane = True
_props = ["Distance"]
class PointsProjectDistance(Base):
_id = 6
_entityDef = (_p,_p,_l)
_props = ["Distance"]
class PointPlaneDistance(Base):
_id = 7
_entityDef = (_p,_w)
_props = ["Distance"]
class PointLineDistance(Base):
_id = 8
_entityDef = (_p,_l)
_workplane = True
_props = ["Distance"]
class EqualLength(Base):
_id = 9
_entityDef = (_l,_l)
_workplane = True
class LengthRatio(Base):
_id = 10
_entityDef = (_l,_l)
_workplane = True
_props = ["Ratio"]
class LengthDifference(Base):
_id = 11
_entityDef = (_l,_l)
_workplane = True
_props = ["Difference"]
class EqualLengthPointLineDistance(Base):
_id = 12
_entityDef = (_p,_l,_l)
_workplane = True
class EqualPointLineDistance(Base):
_id = 13
_entityDef = (_p,_l,_p,_l)
_workplane = True
class EqualAngle(Base):
_id = 14
_entityDef = (_l,_l,_l,_l)
_workplane = True
_props = ["Supplement"]
class EqualLineArcLength(Base):
_id = 15
_entityDef = (_l,_a)
_workplane = True
class Symmetric(Base):
_id = 16
_entityDef = (_p,_p,_w)
_workplane = True
class SymmetricHorizontal(Base):
_id = 17
_entityDef = (_p,_p,_w)
class SymmetricVertical(Base):
_id = 18
_entityDef = (_p,_p,_w)
class SymmetricLine(Base):
_id = 19
_entityDef = (_p,_p,_l,_w)
class MidPoint(Base):
_id = 20
_entityDef = (_p,_p,_l)
_workplane = True
class PointsHorizontal(Base):
_id = 21
_entityDef = (_p,_p)
_workplane = True
class PointsVertical(Base):
_id = 22
_entityDef = (_p,_p)
_workplane = True
class LineHorizontal(Base):
_id = 23
_entityDef = [_l]
_workplane = True
class LineVertical(Base):
_id = 24
_entityDef = [_l]
_workplane = True
class Diameter(Base):
_id = 25
_entityDef = [_c]
_prop = ["Diameter"]
class PointOnCircle(Base):
_id = 26
_entityDef = [_p,_c]
class ArcLineTangent(Base):
_id = 30
_entityDef = (_c,_l)
_props = ["AtEnd"]
# class CubicLineTangent(Base):
# _id = 31
#
#
# class CurvesTangent(Base):
# _id = 32
class EqualRadius(Base):
_id = 33
_entityDef = (_c,_c)
_props = ["Radius"]
class WhereDragged(Base):
_id = 34
_entityDef = [_p]
_workplane = True
TypeEnum = namedtuple('AsmConstraintEnum',
(c.getName() for c in Types))(*range(len(Types)))
def attach(obj,checkType=True):
if checkType:
if 'Type' not in obj.PropertiesList:
# The 'Type' property here is to let user select the type in
# property editor. It is marked as 'transient' to avoid having to
# save the enumeration value for each object.
obj.addProperty("App::PropertyEnumeration","Type","Base",'',2)
obj.Type = TypeEnum._fields
idx = 0
try:
idx = TypeMap[obj._Type]._idx
except AttributeError:
logger.warn('{} has unknown constraint type {}'.format(
objName(obj),obj._Type))
obj.Type = idx
constraintType = TypeNameMap[obj.Type]
cstr = getattr(obj.Proxy,'_cstr',None)
if type(cstr) is not constraintType:
logger.debug('attaching {}, {} -> {}'.format(
objName(obj),type(cstr).__name__,constraintType.__name__),frame=1)
if cstr:
cstr.detach(obj)
obj.Proxy._cstr = constraintType(obj)
obj.ViewObject.signalChangeIcon()
def onChanged(obj,prop):
if prop == 'Type':
if hasattr(obj.Proxy,'_cstr'):
attach(obj,False)
return
elif prop == '_Type':
if hasattr(obj,'Type'):
obj.Type = TypeMap[obj._Type]._idx
return
elif prop == 'Disabled':
obj.ViewObject.signalChangeIcon()
return
cstr = getattr(obj.Proxy,'_cstr',None)
if cstr:
cstr.onChanged(obj,prop)
def check(tp,group):
TypeMap[tp].check(group)
def prepare(obj,solver):
obj.Proxy._cstr.prepare(obj,solver)
def isLocked(obj):
return not obj.Disabled and isinstance(obj.Proxy._cstr,Locked)
def getIcon(obj):
cstr = getattr(obj.Proxy,'_cstr',None)
if cstr:
return cstr.getIcon(obj)