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