FreeCAD_assembly3/constraint.py

568 lines
15 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)
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 cls.slvsFunc():
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
# 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
PartInfo = namedtuple('SolverPartInfo',
('PartName','Placement','Params','Workplane','EntityMap','Group'))
def _addEntity(etype,system,partInfo,key,shape):
key += '.{}'.format(etype)
h = partInfo.EntityMap.get(key,None)
if h:
logger.debug('cache {}: {}'.format(key,h))
return h
if etype == 'p': # point
v = utils.getElementPos(shape)
e = system.addPoint3dV(*v)
elif etype == 'n': # normal
v = utils.getElementNormal(shape)
e = system.addNormal3dV(*v)
else:
raise RuntimeError('unknown entity type {}'.format(etype))
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 _p(system,partInfo,key,shape):
'return a slvs handle of a transformed point derived from "shape"'
if not system:
if utils.hasCenter(shape):
return
return 'a vertex or circular edge/face'
return _addEntity('p',system,partInfo,key,shape)
def _n(system,partInfo,key,shape):
'return a slvs handle of a transformed normal derived from "shape"'
if not system:
if utils.isAxisOfPlane(shape):
return
return 'an edge or face with a surface normal'
return _addEntity('n',system,partInfo,key,shape)
def _l(system,partInfo,key,shape,retAll=False):
'return a pair of slvs handle of the end points of an edge in "shape"'
if not system:
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:
v = shape.Edges[0].Vertexes
p1 = system.addPoint3dV(*v[0].Point)
p2 = system.addPoint3dV(*v[-1].Point)
h = system.addLine(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 _w(system,partInfo,key,shape,retAll=False):
'return a slvs handle of a transformed plane/workplane from "shape"'
if not system:
if utils.isAxisOfPlane(shape):
return
return 'an edge or 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(system,partInfo,key,shape)
n = _n(system,partInfo,key,shape)
h = 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 _c(system,partInfo,key,shape,requireArc=False):
'return a slvs handle of a transformed circle/arc derived from "shape"'
if not system:
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(system,partInfo,key,shape,True)
r = utils.getElementCircular(shape)
if not r:
raise RuntimeError('shape is not cicular')
if isinstance(r,(list,tuple)):
l = _l(system,partInfo,key,shape,True)
h += l[1:]
h = system.addArcOfCircleV(*h,group=partInfo.Group)
elif requireArc:
raise RuntimeError('shape is not an arc')
else:
h = h[1:]
h.append(system.addDistanceV(r))
h = system.addCircle(*h,group=partInfo.Group)
logger.debug('{}: {},{} {}'.format(key,h,partInfo.Group,r))
partInfo.EntityMap[key2] = h
return h
def _a(system,partInfo,key,shape):
return _c(system,partInfo,key,shape,True)
_PropertyDistance = ('Value','Distance','PropertyDistance','Constraint')
_PropertyAngle = ('Value','Angle','PropertyAngle','Constraint')
_PropertyRatio = (None,'Ratio','PropertyFloat','Constraint')
_PropertyDifference = (None,'Difference','PropertyFloat','Constraint')
_PropertyDiameter = (None,'Diameter','PropertyFloat','Constraint')
_PropertyRadius = (None,'Radius','PropertyFloat','Constraint')
_PropertySupplement = (None,'Supplement','PropertyBool','Constraint',
'If True, then the second angle is calculated as 180-angle')
_PropertyAtEnd = (None,'AtEnd','PropertyBool','Constraint',
'If True, then tangent at the end point, or else at the start point')
_ordinal = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th' ]
class Base:
__metaclass__ = ConstraintType
_id = -1
_entities = []
_workplane = False
_props = []
_func = None
_icon = None
_iconDisabled = None
_iconName = 'Assembly_ConstraintGeneral.svg'
def __init__(self,obj,props):
if obj._Type != self._id:
if self._id < 0:
raise RuntimeError('invalid constraint type {} id: '
'{}'.format(self.__class__,self._id))
obj._Type = self._id
for prop in self.__class__._props:
if prop[1] not in props:
obj.addProperty(*prop[1:])
else:
obj.setPropertyStatus(prop[1],'-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,name=None):
entities = cls._entities
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 name:
name = cls.getName()
else:
name += ' of type "{}"'.format(cls.getName)
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._entities):
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):
for prop in cls._props:
# obj.removeProperty(prop[1])
obj.setPropertyStatus(prop[1],'Hidden')
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:
v = getattr(obj,prop[1])
if prop[0]:
v = getattr(v,prop[0])()
ret.append(v)
elements = obj.Proxy.getElements()
entities = cls.getEntityDef(elements,True,objName(obj))
ret = []
for e,o in zip(entities,elements):
info = o.Proxy.getInfo()
partInfo = solver.getPartInfo(info)
ret.append(e(solver.system,partInfo,info.Subname,info.Shape))
logger.debug('{}: {}, {}'.format(objName(obj),obj.Type,ret))
return ret
@classmethod
def prepare(cls,obj,solver):
e = cls.getEntities(obj,solver)
cls._func(solver.system,*e,group=solver.group)
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 PointsCoincident(Base):
_id = 1
_entities = (_p,_p)
_workplane = True
_iconName = 'Assembly_ConstraintCoincidence.svg'
class SameOrientation(Base):
_id = 2
_entities = (_n,_n)
_iconName = 'Assembly_ConstraintOrientation.svg'
class PointInPlane(Base):
_id = 3
_entities = (_p,_w)
class PointOnLine(Base):
_id = 4
_entities = (_p,_l)
_workplane = True
class PointsDistance(Base):
_id = 5
_entities = (_p,_p)
_workplane = True
_props = [_PropertyDistance]
class PointsProjectDistance(Base):
_id = 6
_entities = (_p,_p,_l)
_props = [_PropertyDistance]
class PointPlaneDistance(Base):
_id = 7
_entities = (_p,_w)
_props = [_PropertyDistance]
class PointLineDistance(Base):
_id = 8
_entities = (_p,_l)
_workplane = True
_props = [_PropertyDistance]
class EqualLength(Base):
_id = 9
_entities = (_l,_l)
_workplane = True
class LengthRatio(Base):
_id = 10
_entities = (_l,_l)
_workplane = True
_props = [_PropertyRatio]
class LengthDifference(Base):
_id = 11
_entities = (_l,_l)
_workplane = True
_props = [_PropertyDifference]
class EqualLengthPointLineDistance(Base):
_id = 12
_entities = (_p,_l,_l)
_workplane = True
class EqualPointLineDistance(Base):
_id = 13
_entities = (_p,_l,_p,_l)
_workplane = True
class EqualAngle(Base):
_id = 14
_entities = (_l,_l,_l,_l)
_workplane = True
_props = [_PropertySupplement]
class EqualLineArcLength(Base):
_id = 15
_entities = (_l,_a)
_workplane = True
class Symmetric(Base):
_id = 16
_entities = (_p,_p,_w)
_workplane = True
class SymmetricHorizontal(Base):
_id = 17
_entities = (_p,_p,_w)
class SymmetricVertical(Base):
_id = 18
_entities = (_p,_p,_w)
class SymmetricLine(Base):
_id = 19
_entities = (_p,_p,_l,_w)
class MidPoint(Base):
_id = 20
_entities = (_p,_p,_l)
_workplane = True
class PointsHorizontal(Base):
_id = 21
_entities = (_p,_p)
_workplane = True
class PointsVertical(Base):
_id = 22
_entities = (_p,_p)
_workplane = True
class LineHorizontal(Base):
_id = 23
_entities = [_l]
_workplane = True
class LineVertical(Base):
_id = 24
_entities = [_l]
_workplane = True
class Diameter(Base):
_id = 25
_entities = [_c]
_prop = [_PropertyDiameter]
class PointOnCircle(Base):
_id = 26
_entities = [_p,_c]
class Angle(Base):
_id = 27
_entities = (_l,_l)
_workplane = True
_props = [_PropertyAngle,_PropertySupplement]
class Perpendicular(Base):
_id = 28
_entities = (_l,_l)
_workplane = True
class Parallel(Base):
_id = 29
_entities = (_l,_l)
_workplane = True
class ArcLineTangent(Base):
_id = 30
_entities = (_c,_l)
_props = [_PropertyAtEnd]
# class CubicLineTangent(Base):
# _id = 31
#
#
# class CurvesTangent(Base):
# _id = 32
class EqualRadius(Base):
_id = 33
_entities = (_c,_c)
_props = [_PropertyRadius]
class WhereDragged(Base):
_id = 34
_entities = [_p]
_workplane = True
TypeEnum = namedtuple('AsmConstraintEnum',
(c.getName() for c in Types))(*range(len(Types)))
def attach(obj,checkType=True):
props = None
if checkType:
props = obj.PropertiesList
# if not '_Type' in props:
# raise RuntimeError('Object "{}" has no _Type property'.format(
# objName(obj)))
if 'Type' in props:
raise RuntimeError('Object {} already as property "Type"'.format(
objName(obj)))
# 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","Constraint",'',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:
if cstr:
cstr.detach(obj)
if not props:
props = obj.PropertiesList
obj.Proxy._cstr = constraintType(obj,props)
obj.ViewObject.signalChangeIcon()
def onChanged(obj,prop):
if prop == 'Type':
attach(obj,False)
return
elif prop == '_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):
return obj.Proxy._cstr.getIcon(obj)