From 59c2c7a35d640da7a6b8aa9afe58df6810034aed Mon Sep 17 00:00:00 2001 From: "Zheng, Lei" Date: Wed, 27 Sep 2017 04:03:42 +0800 Subject: [PATCH] Code refactor to prepare for multiple backend solvers --- ...aintDisabled.svg => Assembly_Disabled.svg} | 0 __init__.py | 15 +- assembly.py | 157 +++-- constraint.py | 540 +++++++----------- proxy.py | 238 ++++++++ solver.py | 166 +++--- sys_slvs.py | 45 ++ system.py | 164 ++++++ utils.py | 99 ++-- 9 files changed, 907 insertions(+), 517 deletions(-) rename Gui/Resources/icons/{constraints/Assembly_ConstraintDisabled.svg => Assembly_Disabled.svg} (100%) create mode 100644 proxy.py create mode 100644 sys_slvs.py create mode 100644 system.py diff --git a/Gui/Resources/icons/constraints/Assembly_ConstraintDisabled.svg b/Gui/Resources/icons/Assembly_Disabled.svg similarity index 100% rename from Gui/Resources/icons/constraints/Assembly_ConstraintDisabled.svg rename to Gui/Resources/icons/Assembly_Disabled.svg diff --git a/__init__.py b/__init__.py index 61c20a0..60ed1f9 100644 --- a/__init__.py +++ b/__init__.py @@ -1,10 +1,15 @@ - import FreeCAD, FreeCADGui, Part -import asm3.assembly as assembly -import asm3.constraint as constraint -import asm3.utils as utils -import asm3.solver as solver +from asm3 import utils,assembly,solver,constraint,system +from asm3.utils import logger from asm3.assembly import Assembly,AsmConstraint +try: + from asm3 import sys_slvs +except ImportError as e: + logger.error('failed to import slvs: {}'.format(e)) +try: + from asm3 import sys_sympy +except ImportError as e: + logger.error('failed to import sympy: {}'.format(e)) def test(): doc = FreeCAD.newDocument() diff --git a/assembly.py b/assembly.py index 2d8d538..c546da2 100644 --- a/assembly.py +++ b/assembly.py @@ -1,9 +1,10 @@ import os from collections import namedtuple -from PySide.QtGui import QIcon import FreeCAD, FreeCADGui -import asm3.constraint as constraint -from asm3.utils import logger, objName, iconPath +import asm3.utils as utils +from asm3.utils import logger, objName +from asm3.constraint import Constraint +from asm3.system import System def setupUndo(doc,undoDocs,name='Assembly3 solve'): if doc in undoDocs: @@ -73,16 +74,12 @@ class ViewProviderAsmBase(object): def __setstate__(self, _state): return None - _icon = None _iconName = None @classmethod def getIcon(cls): - if not cls._iconName: - return - if not cls._icon: - cls._icon = QIcon(os.path.join(iconPath, cls._iconName)) - return cls._icon + if cls._iconName: + return utils.getIcon(cls) class AsmGroup(AsmBase): @@ -257,8 +254,9 @@ class AsmElement(AsmBase): if not isTypeOf(element,AsmElement): raise RuntimeError('The second selection must be an element') - return AsmElement.Selection( - link.Assembly,element,link.Subname+subElement) + return AsmElement.Selection(Assembly = link.Assembly, + Element = element, + Subname = link.Subname+subElement) @staticmethod def make(selection=None,name='Element'): @@ -415,6 +413,8 @@ class AsmElementLink(AsmBase): if ret: return ret self.info = None + if not getattr(self,'obj',None): + return assembly = self.getAssembly() subname = self.getShapeSubName() names = subname.split('.') @@ -434,7 +434,8 @@ class AsmElementLink(AsmBase): obj = None if not isTypeOf(part,Assembly,True) and \ - not constraint.isLocked(self.parent.obj): + not Constraint.isDisabled(self.parent.obj) and \ + not Constraint.isLocked(self.parent.obj): getter = getattr(part.getLinkedObject(True),'getLinkExtProperty') # special treatment of link array (i.e. when ElementCount!=0), we @@ -483,8 +484,8 @@ class AsmElementLink(AsmBase): if not shape: # Here means, either the 'part' is an assembly or it is a non array - # object. We trim the subname reference to be after the part object. - # And obtain the shape before part's Placement by setting + # object. We trim the subname reference to be relative to the part + # object. And obtain the shape before part's Placement by setting # 'transform' to False subname = '.'.join(names[1:]) shape = part.getSubObject(subname,transform=False) @@ -492,8 +493,12 @@ class AsmElementLink(AsmBase): obj = part.getLinkedObject(False) partName = part.Name - self.info = AsmElementLink.Info( - part,partName,pla.copy(),obj,subname,shape.copy()) + self.info = AsmElementLink.Info(Part = part, + PartName = partName, + Placement = pla.copy(), + Object = obj, + Subname = subname, + Shape = shape.copy()) return self.info @staticmethod @@ -527,6 +532,9 @@ class AsmElementLink(AsmBase): element.Proxy.setLink(info.Owner,info.Subname) return element +def setPlacement(part,pla,undoDocs): + AsmElementLink.setPlacement(part,pla,undoDocs) + class ViewProviderAsmElementLink(ViewProviderAsmBase): pass @@ -539,20 +547,33 @@ class AsmConstraint(AsmGroup): self.parent = getProxy(parent,AsmConstraintGroup) super(AsmConstraint,self).__init__() - def attach(self,obj): - # Property '_Type' is hidden from editor. The type is for the solver to - # store some internal type id of the constraint, to avoid potential - # problem of version upgrade in the future. The type id is oqaque to the - # objects in this module. The constraint module is reponsible to add the - # actual 'Type' enumeration property that is avaiable for user to change - # in the editor. - obj.addProperty("App::PropertyInteger","_Type","Base",'',0,False,True) - obj.addProperty("App::PropertyBool","Disabled","Base",'') - super(AsmConstraint,self).attach(obj) + def checkSupport(self): + # this function maybe called during document restore, hence the + # extensive check below + obj = getattr(self,'obj',None) + if not obj: + return + if Constraint.isLocked(obj) or \ + Constraint.isDisabled(obj): + return + parent = getattr(self,'parent',None) + if not parent: + return + parent = getattr(parent,'parent',None) + if not parent: + return + assembly = getattr(parent,'obj',None) + if not assembly or \ + System.isConstraintSupported(assembly,Constraint.getTypeName(obj)): + return + raise RuntimeError('Constraint type "{}" is not supported by ' + 'solver "{}"'.format(Constraint.getTypeName(obj), + System.getTypeName(assembly))) def onChanged(self,obj,prop): - constraint.onChanged(obj,prop) super(AsmConstraint,self).onChanged(obj,prop) + if Constraint.onChanged(obj,prop): + obj.recompute() def linkSetup(self,obj): self.elements = None @@ -560,27 +581,33 @@ class AsmConstraint(AsmGroup): obj.setPropertyStatus('VisibilityList','Output') for o in obj.Group: getProxy(o,AsmElementLink).parent = self - constraint.attach(obj) + Constraint.attach(obj) + obj.recompute() def execute(self,_obj): + self.checkSupport() self.getElements(True) return False def getElements(self,refresh=False): if refresh: self.elements = None + obj = getattr(self,'obj',None) + if not obj: + return ret = getattr(self,'elements',None) - obj = self.obj - if ret or obj.Disabled: + if ret or Constraint.isDisabled(obj): return ret shapes = [] elements = [] for o in obj.Group: checkType(o,AsmElementLink) info = o.Proxy.getInfo() + if not info: + return shapes.append(info.Shape) elements.append(o) - constraint.check(obj._Type,shapes) + Constraint.check(obj,shapes) self.elements = elements return self.elements @@ -588,7 +615,7 @@ class AsmConstraint(AsmGroup): ('Assembly','Constraint','Elements')) @staticmethod - def getSelection(tp=0): + def getSelection(typeid=0): ''' Parse Gui.Selection for making a constraint @@ -635,21 +662,23 @@ class AsmConstraint(AsmGroup): elements.append((found.Object,found.Subname)) check = None - if cstr and not cstr.Disabled: - tp = cstr._Type + if cstr and not Constraint.isDisabled(cstr): + typeid = Constraint.getTypeID(cstr) info = cstr.Proxy.getInfo() check = [o.getShape() for o in info.Elements] + elements - elif tp: + elif typeid: check = elements if check: - constraint.check(tp,check) + Constraint.check(typeid,check) - return AsmConstraint.Selection(assembly,cstr,elements) + return AsmConstraint.Selection(Assembly = assembly, + Constraint = cstr, + Elements = elements) @staticmethod - def make(tp, selection=None, name='Constraint'): + def make(typeid, selection=None, name='Constraint'): if not selection: - selection = AsmConstraint.getSelection(tp) + selection = AsmConstraint.getSelection(typeid) if selection.Constraint: cstr = selection.Constraint else: @@ -658,7 +687,7 @@ class AsmConstraint(AsmGroup): name,AsmConstraint(constraints),None,True) ViewProviderAsmConstraint(cstr.ViewObject) constraints.setLink({-1:cstr}) - cstr._Type = tp + Constraint.setTypeID(cstr,typeid) for e in selection.Elements: AsmElementLink.make(AsmElementLink.MakeInfo(cstr,*e)) @@ -676,7 +705,7 @@ class ViewProviderAsmConstraint(ViewProviderAsmGroup): return (1.0,60.0/255.0,60.0/255.0) def getIcon(self): - return constraint.getIcon(self.ViewObject.Object) + return Constraint.getIcon(self.ViewObject.Object) class AsmConstraintGroup(AsmGroup): @@ -688,7 +717,10 @@ class AsmConstraintGroup(AsmGroup): super(AsmConstraintGroup,self).linkSetup(obj) obj.setPropertyStatus('VisibilityList','Output') for o in obj.Group: - getProxy(o,AsmConstraint).parent = self + cstr = getProxy(o,AsmConstraint) + if cstr: + cstr.parent = self + obj.recompute() @staticmethod def make(parent,name='Constraints'): @@ -747,21 +779,28 @@ class ViewProviderAsmElementGroup(ViewProviderAsmBase): vobj.Object.Proxy.parent.obj,None,subname)) -BuildShapeNames = ('No','Compound','Fuse','Cut') -BuildShapeEnum = namedtuple('AsmBuildShapeEnum',BuildShapeNames)( - *range(len(BuildShapeNames))) - +BuildShapeNone = 'None' +BuildShapeCompound = 'Compound' +BuildShapeFuse = 'Fuse' +BuildShapeCut = 'Cut' +BuildShapeNames = (BuildShapeNone,BuildShapeCompound, + BuildShapeFuse,BuildShapeCut) class Assembly(AsmGroup): def __init__(self): self.constraints = None super(Assembly,self).__init__() - def execute(self,_obj): + def execute(self,obj): self.constraints = None self.buildShape() + System.touch(obj) return False # return False to call LinkBaseExtension::execute() + def onSolverChanged(self): + for obj in self.getConstraintGroup().Group: + obj.recompute() + def buildShape(self): obj = self.obj if not obj.BuildShape: @@ -774,7 +813,7 @@ class Assembly(AsmGroup): group = partGroup.Group if not group: raise RuntimeError('no parts') - if obj.BuildShape == BuildShapeEnum.Cut: + if obj.BuildShape == BuildShapeCut: shape = Part.getShape(group[0]).Solids if not shape: raise RuntimeError('First part has no solid') @@ -789,9 +828,9 @@ class Assembly(AsmGroup): raise RuntimeError('No solids found in parts') if len(shape) == 1: obj.Shape = shape[0] - elif obj.BuildShape == BuildShapeEnum.Fuse: + elif obj.BuildShape == BuildShapeFuse: obj.Shape = shape[0].fuse(shape[1:]) - elif obj.BuildShape == BuildShapeEnum.Cut: + elif obj.BuildShape == BuildShapeCut: if len(shape)>2: obj.Shape = shape[0].cut(shape[1].fuse(shape[2:])) else: @@ -807,6 +846,8 @@ class Assembly(AsmGroup): def linkSetup(self,obj): obj.configLinkProperty('Placement') super(Assembly,self).linkSetup(obj) + obj.setPropertyStatus('VisibilityList','Output') + System.attach(obj) self.onChanged(obj,'BuildShape') # make sure all children are there, first constraint group, then element @@ -814,13 +855,17 @@ class Assembly(AsmGroup): # all groups exist. The order of the group is important to make sure # correct rendering and picking behavior self.getPartGroup(True) + self.onSolverChanged() def onChanged(self, obj, prop): if prop == 'BuildShape': - if not obj.BuildShape or obj.BuildShape == BuildShapeEnum.Compound: + if not obj.BuildShape or obj.BuildShape == BuildShapeCompound: obj.setPropertyStatus('Shape','-Transient') else: obj.setPropertyStatus('Shape','Transient') + return + System.onChanged(obj,prop) + super(Assembly,self).onChanged(obj,prop) def getConstraintGroup(self, create=False): obj = self.obj @@ -853,7 +898,7 @@ class Assembly(AsmGroup): ret = [] for o in cstrGroup.Group: checkType(o,AsmConstraint) - if o.Disabled: + if Constraint.isDisabled(o): logger.debug('skip constraint "{}" type ' '{}'.format(objName(o),o.Type)) continue @@ -963,7 +1008,9 @@ class Assembly(AsmGroup): return subs = subs[idx:] - ret = Assembly.Info(assembly,child,'.'.join(subs)) + ret = Assembly.Info(Assembly = assembly, + Object = child, + Subname = '.'.join(subs)) if not recursive: return ret @@ -989,7 +1036,6 @@ class Assembly(AsmGroup): class ViewProviderAssembly(ViewProviderAsmGroup): - _iconName = 'Assembly_Assembly_Tree.svg' def canDragObject(self,_child): return False @@ -1003,3 +1049,6 @@ class ViewProviderAssembly(ViewProviderAsmGroup): def canDropObjects(self): return False + def getIcon(self): + return System.getIcon(self.ViewObject.Object) + diff --git a/constraint.py b/constraint.py index 6fd86c6..897221d 100644 --- a/constraint.py +++ b/constraint.py @@ -1,260 +1,239 @@ +from future.utils import with_metaclass 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 +from asm3.proxy import ProxyType, PropertyInfo, propGet, propGetValue import os -iconPath = os.path.join(utils.iconPath,'constraints') -pixmapDisabled = QPixmap(os.path.join( - iconPath,'Assembly_ConstraintDisabled.svg')) -iconSize = (16,16) +_iconPath = os.path.join(utils.iconPath,'constraints') -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"' +def _p(solver,partInfo,subname,shape): + 'return a 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' + key = subname+'.p' h = partInfo.EntityMap.get(key,None) + system = solver.system if h: - logger.debug('cache {}: {}'.format(key,h)) + system.log('cache {}: {}'.format(key,h)) else: v = utils.getElementPos(shape) - system = solver.system + system.NameTag = subname e = system.addPoint3dV(*v) + system.NameTag = partInfo.PartName h = system.addTransform(e,*partInfo.Params,group=partInfo.Group) - logger.debug('{}: {},{}, {}, {}'.format(key,h,partInfo.Group,e,v)) + system.log('{}: {},{}'.format(key,h,partInfo.Group)) 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' +def _n(solver,partInfo,subname,shape): + 'return a 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' + key = subname+'.n' h = partInfo.EntityMap.get(key,None) + system = solver.system if h: - logger.debug('cache {}: {}'.format(key,h)) + system.log('cache {}: {}'.format(key,h)) else: - system = solver.system - params = [ system.addParamV(n) for n in utils.getElementNormal(shape) ] - e = system.addNormal3d(*params) + system.NameTag = subname + e = system.addNormal3dV(*utils.getElementNormal(shape)) + system.NameTag = partInfo.PartName h = system.addTransform(e,*partInfo.Params,group=partInfo.Group) - h = [h,e,params] - logger.debug('{}: {},{}'.format(key,h,partInfo.Group)) + system.log('{}: {},{}'.format(key,h,partInfo.Group)) partInfo.EntityMap[key] = h - return h if retAll else h[0] + return h -def _l(solver,partInfo,key,shape,retAll=False): - 'return a pair of slvs handle of the end points of an edge in "shape"' +def _l(solver,partInfo,subname,shape,retAll=False): + 'return a pair of handle of the end points of an edge in "shape"' if not solver: if utils.isLinearEdge(shape): return return 'a linear edge' - key += '.l' + key = subname+'.l' h = partInfo.EntityMap.get(key,None) + system = solver.system if h: - logger.debug('cache {}: {}'.format(key,h)) + system.log('cache {}: {}'.format(key,h)) else: - system = solver.system + system.NameTag = subname 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)) + system.NameTag = partInfo.PartName + tp1 = system.addTransform(p1,*partInfo.Params,group=partInfo.Group) + tp2 = system.addTransform(p2,*partInfo.Params,group=partInfo.Group) + h = system.addLineSegment(tp1,tp2,group=partInfo.Group) + h = (h,tp1,tp2,p1,p2) + system.log('{}: {},{}'.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' +def _ln(solver,partInfo,subname,shape,retAll=False): + 'return a 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) + return _l(solver,partInfo,subname,shape,retAll) + return _n(solver,partInfo,subname,shape) -def _w(solver,partInfo,key,shape,retAll=False): - 'return a slvs handle of a transformed plane/workplane from "shape"' +def _w(solver,partInfo,subname,shape,retAll=False): + 'return a 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) + key = subname+'.w' + h = partInfo.EntityMap.get(key,None) + system = solver.system if h: - logger.debug('cache {}: {}'.format(key,h)) + system.log('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) + p = _p(solver,partInfo,subname,shape) + n = _n(solver,partInfo,subname,shape) + system.NameTag = partInfo.PartName + h = system.addWorkplane(p,n,group=partInfo.Group) h = (h,p,n) - logger.debug('{}: {},{}'.format(key,h,partInfo.Group)) - partInfo.EntityMap[key2] = h + system.log('{}: {},{}'.format(key,h,partInfo.Group)) + partInfo.EntityMap[key] = h return h if retAll else h[0] -def _wa(solver,partInfo,key,shape): - return _w(solver,partInfo,key,shape,True) +def _wa(solver,partInfo,subname,shape): + return _w(solver,partInfo,subname,shape,True) -def _c(solver,partInfo,key,shape,requireArc=False): - 'return a slvs handle of a transformed circle/arc derived from "shape"' +def _c(solver,partInfo,subname,shape,requireArc=False): + 'return a 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) + key = subname+'.c' + h = partInfo.EntityMap.get(key,None) + system = solver.system if h: - logger.debug('cache {}: {}'.format(key,h)) + system.log('cache {}: {}'.format(key,h)) else: - h = _w(solver,partInfo,key,shape,True) + h = [_w(solver,partInfo,subname,shape,False)] 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) + l = _l(solver,partInfo,subname,shape,True) h += l[1:] - h = solver.system.addArcOfCircleV(*h,group=partInfo.Group) + system.NameTag = partInfo.PartName + h = system.addArcOfCircleV(*h,group=partInfo.Group) elif requireArc: raise RuntimeError('shape is not an arc') else: - h = h[1:] + system.NameTag = partInfo.PartName 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 + h = system.addCircle(*h,group=partInfo.Group) + system.log('{}: {},{}'.format(key,h,partInfo.Group)) + partInfo.EntityMap[key] = h return h -def _a(solver,partInfo,key,shape): - return _c(solver,partInfo,key,shape,True) +def _a(solver,partInfo,subname,shape): + return _c(solver,partInfo,subname,shape,True) -class Base: - __metaclass__ = ConstraintType +class Constraint(ProxyType): + 'constraint meta class' + _typeID = '_ConstraintType' + _typeEnum = 'ConstraintType' + + _disabled = 'Disabled' + + @classmethod + def attach(mcs,obj,checkType=True): + if checkType: + if not mcs._disabled in obj.PropertiesList: + obj.addProperty("App::PropertyBool",mcs._disabled,"Base",'') + return super(Constraint,mcs).attach(obj,checkType) + + @classmethod + def onChanged(mcs,obj,prop): + if prop == mcs._disabled: + obj.ViewObject.signalChangeIcon() + return + return super(Constraint,mcs).onChanged(obj,prop) + + @classmethod + def isDisabled(mcs,obj): + return getattr(obj,mcs._disabled,False) + + @classmethod + def check(mcs,tp,group): + mcs.getType(tp).check(group) + + @classmethod + def prepare(mcs,obj,solver): + return mcs.getProxy(obj).prepare(obj,solver) + + @classmethod + def isLocked(mcs,obj): + return isinstance(mcs.getProxy(obj),Locked) + + @classmethod + def getIcon(mcs,obj): + cstr = mcs.getProxy(obj) + if cstr: + return cstr.getIcon(obj) + + +def _makeProp(name,tp,doc='',getter=propGet,internal=False): + PropertyInfo(Constraint,name,tp,doc,getter=getter, + group='Constraint',internal=internal) + +_makeProp('Distance','App::PropertyDistance',getter=propGetValue) +_makeProp('Offset','App::PropertyDistance',getter=propGetValue) +_makeProp('Cascade','App::PropertyBool',internal=True) +_makeProp('Angle','App::PropertyAngle',getter=propGetValue) +_makeProp('Ratio','App::PropertyFloat') +_makeProp('Difference','App::PropertyFloat') +_makeProp('Diameter','App::PropertyFloat') +_makeProp('Radius','App::PropertyFloat') +_makeProp('Supplement','App::PropertyBool', + 'If True, then the second angle is calculated as 180-angle') +_makeProp('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') + +def cstrName(obj): + return '{}<{}>'.format(objName(obj),Constraint.getTypeName(obj)) + + +class Base(with_metaclass(Constraint,object)): _id = -1 - _entityDef = [] + _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') + def __init__(self,_obj): + self._supported = True @classmethod - def getName(cls): - return cls.__name__ + def getPropertyInfoList(cls): + return cls._props @classmethod - def slvsFunc(cls): + def constraintFunc(cls,obj,solver): try: - if not cls._func: - cls._func = getattr(slvs.System,'add'+cls.getName()) - return cls._func + return getattr(solver.system,'add'+cls.getName()) except AttributeError: - logger.error('Invalid slvs constraint "{}"'.format(cls.getName())) + logger.warn('{} not supported in solver "{}"'.format( + cstrName(obj),solver.getName())) @classmethod def getEntityDef(cls,group,checkCount,obj=None): @@ -284,46 +263,22 @@ class Base: if not msg: continue if i == len(cls._entityDef): - raise RuntimeError('Constraint {} requires the optional {} ' + 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' + 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 + return utils.getIcon(cls,Constraint.isDisabled(obj),_iconPath) @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)) - + '''maps fcad element shape to entities''' elements = obj.Proxy.getElements() entities = cls.getEntityDef(elements,True,obj) + ret = [] for e,o in zip(entities,elements): info = o.Proxy.getInfo() partInfo = solver.getPartInfo(info) @@ -333,14 +288,16 @@ class Base: @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)) + func = cls.constraintFunc(obj,solver) + if func: + params = cls.getPropertyValues(obj) + cls.getEntities(obj,solver) + return func(*params,group=solver.group) + else: + logger.warn('{} no constraint func'.format(cstrName(obj))) class Locked(Base): _id = 0 - _func = True _iconName = 'Assembly_ConstraintLock.svg' @classmethod @@ -354,26 +311,31 @@ class Locked(Base): class BaseMulti(Base): _id = -1 - _func = True - _entityDef = [_wa] + _entityDef = (_wa,) @classmethod def check(cls,group): if len(group)<2: - raise RuntimeError('Constraint {} requires at least two ' + 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 ' + raise RuntimeError('Constraint "{}" requires all the element ' 'to be of {}'.format(cls.getName())) return @classmethod def prepare(cls,obj,solver): + func = cls.constraintFunc(obj,solver); + if not func: + logger.warn('{} no constraint func'.format(cstrName(obj))) + return parts = set() ref = None elements = [] + props = cls.getPropertyValues(obj) + for e in obj.Proxy.getElements(): info = e.Proxy.getInfo() if info.Part in parts: @@ -394,6 +356,7 @@ class BaseMulti(Base): logger.warn('{} has no effective constraint'.format(cstrName(obj))) return e0 = None + ret = [] for e in elements: info = e.Proxy.getInfo() partInfo = solver.getPartInfo(info) @@ -401,111 +364,74 @@ class BaseMulti(Base): 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) + params = props + [e0,e] + h = func(*params,group=solver.group) + if isinstance(h,(list,tuple)): + ret += list(h) + else: + ret.append(h) + return ret class BaseCascade(BaseMulti): @classmethod def prepare(cls,obj,solver): if not getattr(obj,'Cascade',True): - super(BaseCascade,cls).prepare(obj,solver) + return super(BaseCascade,cls).prepare(obj,solver) + func = cls.constraintFunc(obj,solver); + if not func: + logger.warn('{} no constraint func'.format(cstrName(obj))) return + props = cls.getPropertyValues(obj) prev = None - count = 0 + ret = [] 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: + params = props + [e1,e2] + else: + params = props + [e2,e1] + h = func(*params,group=solver.group) + if isinstance(h,(list,tuple)): + ret += list(h) + else: + ret.append(h) + + if not ret: logger.warn('{} has no effective constraint'.format(cstrName(obj))) + return ret 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)) + _props = ['Cascade','Offset'] 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)) + _props = ['Cascade','Offset'] 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] + _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 @@ -531,15 +457,9 @@ class Parallel(Base): class MultiParallel(BaseMulti): _id = 291 - _entityDef = [_ln] + _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 @@ -670,25 +590,25 @@ class PointsVertical(Base): class LineHorizontal(Base): _id = 23 - _entityDef = [_l] + _entityDef = (_l,) _workplane = True class LineVertical(Base): _id = 24 - _entityDef = [_l] + _entityDef = (_l,) _workplane = True class Diameter(Base): _id = 25 - _entityDef = [_c] - _prop = ["Diameter"] + _entityDef = (_c,) + _prop = ("Diameter",) class PointOnCircle(Base): _id = 26 - _entityDef = [_p,_c] + _entityDef = (_p,_c) class ArcLineTangent(Base): @@ -708,72 +628,10 @@ class ArcLineTangent(Base): class EqualRadius(Base): _id = 33 _entityDef = (_c,_c) - _props = ["Radius"] class WhereDragged(Base): _id = 34 - _entityDef = [_p] + _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) diff --git a/proxy.py b/proxy.py new file mode 100644 index 0000000..9cb0b04 --- /dev/null +++ b/proxy.py @@ -0,0 +1,238 @@ +import past.builtins as pb +from collections import namedtuple +from asm3.utils import logger, objName + +def propGet(self,obj): + return getattr(obj,self.Name) + +def propGetValue(self,obj): + return getattr(getattr(obj,self.Name),'Value') + +class PropertyInfo(object): + def __init__(self,host,name,tp,doc='', enum=None, + getter=propGet,group='Base',internal=False,duplicate=False): + self.Name = name + self.Type = tp + self.Group = group + self.Doc = doc + self.Enum = enum + self.get = getter.__get__(self,self.__class__) + self.Internal = internal + self.Key = host.addPropertyInfo(self,duplicate) + +class ProxyType(type): + _typeID = '_ProxyType' + _typeEnum = 'ProxyType' + _typeGroup = 'Base' + _proxyName = '_proxy' + _registry = {} + + Info = namedtuple('ProxyTypeInfo', + ('Types','TypeMap','TypeNameMap','TypeNames','PropInfo')) + + @classmethod + def getMetaName(mcs): + return mcs.__name__ + + @classmethod + def getInfo(mcs): + if not getattr(mcs,'_info',None): + mcs._info = mcs.Info([],{},{},[],{}) + mcs._registry[mcs.getMetaName()] = mcs._info + return mcs._info + + @classmethod + def reload(mcs): + info = mcs.getInfo() + mcs._info = None + for tp in info.Types: + tp._idx = -1 + mcs.getInfo().Types.append(tp) + mcs.register(tp) + + @classmethod + def getType(mcs,tp): + if isinstance(tp,pb.basestring): + return mcs.getInfo().TypeNameMap[tp] + if not isinstance(tp,int): + tp = mcs.getTypeID(tp) + return mcs.getInfo().TypeMap[tp] + + @classmethod + def getTypeID(mcs,obj): + return getattr(obj,mcs._typeID,-1) + + @classmethod + def setTypeID(mcs,obj,tp): + setattr(obj,mcs._typeID,tp) + + @classmethod + def getTypeName(mcs,obj): + return getattr(obj,mcs._typeEnum,None) + + @classmethod + def setTypeName(mcs,obj,tp): + setattr(obj,mcs._typeEnum,tp) + + @classmethod + def getProxy(mcs,obj): + return getattr(obj.Proxy,mcs._proxyName,None) + + @classmethod + def setProxy(mcs,obj): + cls = mcs.getType(mcs.getTypeName(obj)) + proxy = mcs.getProxy(obj) + if type(proxy) is not cls: + logger.debug('attaching {}, {} -> {}'.format( + objName(obj),type(proxy).__name__,cls.__name__),frame=1) + if proxy: + mcs.detach(obj) + if mcs.getTypeID(obj) != cls._id: + mcs.setTypeID(obj,cls._id) + + props = cls.getPropertyInfoList() + if props: + oprops = obj.PropertiesList + for key in props: + prop = mcs.getPropertyInfo(key) + if prop.Name not in oprops: + 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') + + setattr(obj.Proxy,mcs._proxyName,cls(obj)) + obj.ViewObject.signalChangeIcon() + return obj + + @classmethod + def detach(mcs,obj,detachAll=False): + proxy = mcs.getProxy(obj) + if proxy: + logger.debug('detaching {}<{}>'.format(objName(obj), + proxy.__class__.__name__)) + for key in proxy.getPropertyInfoList(): + prop = mcs.getPropertyInfo(key) + # obj.setPropertyStatus(prop.Name,'Hidden') + obj.removeProperty(prop.Name) + callback = getattr(proxy,'onDetach',None) + if callback: + callback(obj) + setattr(obj.Proxy,mcs._proxyName,None) + + if detachAll: + obj.removeProperty(mcs._typeID) + obj.removeProperty(mcs._typeEnum) + + @classmethod + def setDefaultTypeID(mcs,obj,name=None): + info = mcs.getInfo() + if not name: + name = info.TypeNames[0] + mcs.setTypeID(obj,info.TypeNameMap[name]._id) + + @classmethod + def attach(mcs,obj,checkType=True): + info = mcs.getInfo() + if not info.TypeNames: + logger.error('"{}" has no registered types'.format( + mcs.getMetaName())) + return + + if checkType: + if mcs._typeID not in obj.PropertiesList: + obj.addProperty("App::PropertyInteger", + mcs._typeID,mcs._typeGroup,'',0,False,True) + mcs.setDefaultTypeID(obj) + + if mcs._typeEnum not in obj.PropertiesList: + logger.debug('type enum {}, {}'.format(mcs._typeEnum, + mcs._typeGroup)) + obj.addProperty("App::PropertyEnumeration", + mcs._typeEnum,mcs._typeGroup,'',2) + mcs.setTypeName(obj,info.TypeNames) + + idx = 0 + try: + idx = mcs.getType(obj)._idx + except KeyError: + logger.warn('{} has unknown {} type {}'.format( + objName(obj),mcs.getMetaName(),mcs.getTypeID(obj))) + mcs.setTypeName(obj,idx) + + return mcs.setProxy(obj) + + @classmethod + def onChanged(mcs,obj,prop): + if prop == mcs._typeEnum: + if mcs.getProxy(obj): + return mcs.attach(obj,False) + elif prop == mcs._typeID: + if mcs.getProxy(obj): + cls = mcs.getType(mcs.getTypeID(obj)) + if mcs.getTypeName(obj)!=cls.getName(): + mcs.setTypeName(obj,cls._idx) + + def __init__(cls, name, bases, attrs): + super(ProxyType,cls).__init__(name,bases,attrs) + cls._idx = -1 + mcs = cls.__class__ + mcs.getInfo().Types.append(cls) + mcs.register(cls) + + @classmethod + def register(mcs,cls): + ''' + Register a class to this meta class + + To make the registration automatic at the class definition time, simply + set __metaclass__ of that class to ProxyType of its derived type. + + You can also call this methode directly to register an unrelated class + ''' + if cls._id < 0: + return + info = mcs.getInfo() + if cls._id in info.TypeMap: + raise RuntimeError('Duplicate {} type id {}'.format( + mcs.getMetaName(),cls._id)) + info.TypeMap[cls._id] = cls + info.TypeNameMap[cls.getName()] = cls + info.TypeNames.append(cls.getName()) + cls._idx = len(info.TypeNames)-1 + logger.trace('register {} "{}":{},{}'.format( + mcs.getMetaName(),cls.getName(),cls._id,cls._idx)) + + @classmethod + def addPropertyInfo(mcs,info,duplicate): + props = mcs.getInfo().PropInfo + key = info.Name + i = 1 + while key in props: + if not duplicate: + raise RuntimeError('Duplicate property "{}"'.format(info.Name)) + key = key+str(i) + i = i+1 + props[key] = info + return key + + @classmethod + def getPropertyInfo(mcs,key): + return mcs.getInfo().PropInfo[key] + + def getPropertyValues(cls,obj): + props = [] + mcs = cls.__class__ + for key in cls.getPropertyInfoList(): + prop = mcs.getPropertyInfo(key) + if not prop.Internal: + props.append(prop.get(obj)) + return props + + def getPropertyInfoList(cls): + return [] + + def getName(cls): + return cls.__name__ + diff --git a/solver.py b/solver.py index c60e418..e074ddc 100644 --- a/solver.py +++ b/solver.py @@ -1,24 +1,36 @@ +import random +from collections import namedtuple import FreeCAD, FreeCADGui -import asm3.slvs as slvs import asm3.assembly as asm from asm3.utils import logger, objName, isSamePlacement -import asm3.constraint as constraint -import random +from asm3.constraint import Constraint, cstrName +from asm3.system import System -class AsmSolver(object): +# PartName: text name of the part +# Placement: the original placement of the part +# Params: 7 parameters that defines the 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 +PartInfo = namedtuple('SolverPartInfo', + ('PartName','Placement','Params','Workplane','EntityMap','Group')) + +class Solver(object): def __init__(self,assembly,reportFailed): + self.system = System.getSystem(assembly) cstrs = assembly.Proxy.getConstraints() if not cstrs: - logger.debug('no constraint found in assembly ' + self.system.log('no constraint found in assembly ' '{}'.format(objName(assembly))) return parts = assembly.Proxy.getPartGroup().Group if len(parts)<=1: - logger.debug('not enough parts in {}'.format(objName(assembly))) + self.system.log('not enough parts in {}'.format(objName(assembly))) return - self.system = slvs.System() self._fixedGroup = 2 self.group = 1 # the solving group self._partMap = {} @@ -29,38 +41,33 @@ class AsmSolver(object): self.system.GroupHandle = self._fixedGroup - # convenience constants - self.zero = self.system.addParamV(0) - self.one = self.system.addParamV(1) - self.o = self.system.addPoint3d(self.zero,self.zero,self.zero) - self.x = self.system.addPoint3d(self.one,self.zero,self.zero) - self.y = self.system.addPoint3d(self.zero,self.one,self.zero) - self.z = self.system.addPoint3d(self.zero,self.zero,self.one) - for cstr in cstrs: - if constraint.isLocked(cstr): - constraint.prepare(cstr,self) - else: + if Constraint.isLocked(cstr): + Constraint.prepare(cstr,self) + elif not Constraint.isDisabled(cstr) and \ + System.isConstraintSupported( + assembly,Constraint.getTypeName(cstr)): self._cstrs.append(cstr) if not self._fixedParts: - logger.debug('lock first part {}'.format(objName(parts[0]))) + self.system.log('lock first part {}'.format(objName(parts[0]))) self._fixedParts.add(parts[0]) for cstr in self._cstrs: - logger.debug('preparing {}, type {}'.format( - objName(cstr),cstr.Type)) + self.system.log('preparing {}'.format(cstrName(cstr))) self.system.GroupHandle += 1 - handle = self.system.ConstraintHandle - constraint.prepare(cstr,self) - handles = range(handle+1,self.system.ConstraintHandle+1) - for h in handles: - self._cstrMap[h] = cstr - logger.debug('{} handles: {}'.format(objName(cstr),handles)) + ret = Constraint.prepare(cstr,self) + if ret: + if isinstance(ret,(list,tuple)): + for h in ret: + self._cstrMap[h] = cstr + else: + self._cstrMap[ret] = cstr - logger.debug('solving {}'.format(objName(assembly))) - ret = self.system.solve(group=self.group,reportFailed=reportFailed) - if ret: - if reportFailed: + self.system.log('solving {}'.format(objName(assembly))) + try: + self.system.solve(group=self.group,reportFailed=reportFailed) + except RuntimeError as e: + if reportFailed and self.system.Failed: msg = 'List of failed constraint:' for h in self.system.Failed: cstr = self._cstrMap.get(h,None) @@ -72,25 +79,11 @@ class AsmSolver(object): ' {}'.format(c.group)) continue cstr = self._cstrs[c.group-self._fixedGroup] - msg += '\n{}, type: {}, handle: {}'.format( - objName(cstr),cstr.Type,h) + msg += '\n{}, handle: {}'.format(cstrName(cstr),h) logger.error(msg) - if ret==1: - reason = 'inconsistent constraints' - elif ret==2: - reason = 'not converging' - elif ret==3: - reason = 'too many unknowns' - elif ret==4: - reason = 'init failed' - elif ret==5: - reason = 'redundent constraints' - else: - reason = 'unknown failure' raise RuntimeError('Failed to solve {}: {}'.format( - objName(assembly),reason)) - - logger.debug('done sloving, dof {}'.format(self.system.Dof)) + assembly,e.message)) + self.system.log('done sloving') undoDocs = set() for part,partInfo in self._partMap.items(): @@ -101,11 +94,11 @@ class AsmSolver(object): q = (params[4],params[5],params[6],params[3]) pla = FreeCAD.Placement(FreeCAD.Vector(*p),FreeCAD.Rotation(*q)) if isSamePlacement(partInfo.Placement,pla): - logger.debug('not moving {}'.format(partInfo.PartName)) + self.system.log('not moving {}'.format(partInfo.PartName)) else: - logger.debug('moving {} {} {} {}'.format( + self.system.log('moving {} {} {} {}'.format( partInfo.PartName,partInfo.Params,params,pla)) - asm.AsmElementLink.setPlacement(part,pla,undoDocs) + asm.setPlacement(part,pla,undoDocs) for doc in undoDocs: doc.commitTransaction() @@ -114,7 +107,7 @@ class AsmSolver(object): return info.Part in self._fixedParts def addFixedPart(self,info): - logger.debug('lock part ' + info.PartName) + self.system.log('lock part ' + info.PartName) self._fixedParts.add(info.Part) def getPartInfo(self,info): @@ -144,47 +137,54 @@ class AsmSolver(object): entityMap = {} self._entityMap[info.Object] = entityMap - pla = info.Placement if info.Part in self._fixedParts: g = self._fixedGroup else: g = self.group - q = pla.Rotation.Q - vals = list(pla.Base) + [q[3],q[0],q[1],q[2]] - params = [self.system.addParamV(v,g) for v in vals] + + self.system.NameTag = info.PartName + params = self.system.addPlacement(info.Placement,group=g) p = self.system.addPoint3d(*params[:3],group=g) n = self.system.addNormal3d(*params[3:],group=g) w = self.system.addWorkplane(p,n,group=g) h = (w,p,n) - rparams = [self.zero]*3 + params[3:] - x = self.system.addTransform(self.x,*rparams,group=g) - y = self.system.addTransform(self.y,*rparams,group=g) - z = self.system.addTransform(self.z,*rparams,group=g) + partInfo = PartInfo(PartName = info.PartName, + Placement = info.Placement.copy(), + Params = params, + Workplane = h, + EntityMap = entityMap, + Group = g) - partInfo = constraint.PartInfo(info.PartName, info.Placement.copy(), - params,rparams,h,entityMap,g,x,y,z) - - logger.debug('{}'.format(partInfo)) + self.system.log('{}'.format(partInfo)) self._partMap[info.Part] = partInfo return partInfo - def solve(objs=None,recursive=True,reportFailed=True,recompute=True): if not objs: objs = FreeCAD.ActiveDocument.Objects elif not isinstance(objs,(list,tuple)): objs = [objs] - if not objs: - logger.error('no objects') + assemblies = [] + for obj in objs: + if not asm.isTypeOf(obj,asm.Assembly): + continue + if System.isDisabled(obj): + logger.debug('bypass disabled assembly {}'.format(objName(obj))) + continue + logger.debug('adding assembly {}'.format(objName(obj))) + assemblies.append(obj) + + if not assemblies: + logger.error('no assembly found') return if recompute: docs = set() - for o in objs: + for o in assemblies: docs.add(o.Document) for d in docs: logger.debug('recomputing {}'.format(d.Name)) @@ -193,21 +193,31 @@ def solve(objs=None,recursive=True,reportFailed=True,recompute=True): if recursive: # Get all dependent object, including external ones, and return as a # topologically sorted list. - objs = FreeCAD.getDependentObjects(objs,False,True) - - assemblies = [] - for obj in objs: - if asm.isTypeOf(obj,asm.Assembly): + # + # TODO: it would be ideal if we can filter out those disabled assemblies + # found during the recrusive search. Can't think of an easy way right + # now + objs = FreeCAD.getDependentObjects(assemblies,False,True) + assemblies = [] + for obj in objs: + if not asm.isTypeOf(obj,asm.Assembly): + continue + if System.isDisabled(obj): + logger.debug('skip disabled assembly {}'.format(objName(obj))) + continue + if not System.isTouched(obj): + logger.debug('skip untouched assembly {}'.format(objName(obj))) + continue logger.debug('adding assembly {}'.format(objName(obj))) assemblies.append(obj) - if not assemblies: - logger.error('no assembly found') - return + if not assemblies: + logger.error('no assembly found') + return for assembly in assemblies: - logger.debug('solving assembly {}'.format(objName(assembly))) - AsmSolver(assembly,reportFailed) + Solver(assembly,reportFailed) if recompute: assembly.Document.recompute() + System.touch(assembly,False) diff --git a/sys_slvs.py b/sys_slvs.py new file mode 100644 index 0000000..5e5ff56 --- /dev/null +++ b/sys_slvs.py @@ -0,0 +1,45 @@ +from future.utils import with_metaclass +from asm3.system import System, SystemBase, SystemExtension +from asm3.utils import logger, objName +import asm3.py_slvs.slvs as slvs + +class SystemSlvs(with_metaclass(System,SystemBase)): + _id = 1 + + def __init__(self,obj): + super(SystemSlvs,self).__init__(obj) + + @classmethod + def getName(cls): + return 'SolverSpace' + + def isDisabled(self,_obj): + return False + + def getSystem(self,_obj): + return _SystemSlvs(self.log) + + +class _SystemSlvs(slvs.System, SystemExtension): + def __init__(self,log): + super(_SystemSlvs,self).__init__() + self.log = log + + def solve(self, group=0, reportFailed=False): + ret = super(_SystemSlvs,self).solve(group,reportFailed) + if ret: + if ret==1: + reason = 'inconsistent constraints' + elif ret==2: + reason = 'not converging' + elif ret==3: + reason = 'too many unknowns' + elif ret==4: + reason = 'init failed' + elif ret==5: + reason = 'redundent constraints' + else: + reason = 'unknown failure' + raise RuntimeError(reason) + self.log('dof remaining: {}'.format(self.Dof)) + diff --git a/system.py b/system.py new file mode 100644 index 0000000..0f0f8ec --- /dev/null +++ b/system.py @@ -0,0 +1,164 @@ +import os +from future.utils import with_metaclass +import asm3.utils as utils +from asm3.utils import logger, objName +from asm3.proxy import ProxyType, PropertyInfo + +class System(ProxyType): + 'solver system meta class' + + _typeID = '_SolverType' + _typeEnum = 'SolverType' + _typeGroup = '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 utils.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: + return proxy.getSystem(obj) + + @classmethod + def isConstraintSupported(mcs,obj,cstrName): + proxy = mcs.getProxy(obj) + if proxy: + return proxy.isConstraintSupported(cstrName) + +def _makePropInfo(name,tp,doc=''): + PropertyInfo(System,name,tp,doc,group='Solver') + +_makePropInfo('Verbose','App::PropertyBool') + +class SystemBase(with_metaclass(System,object)): + _id = 0 + _props = ['Verbose'] + + def __init__(self,obj): + self._touched = True + self.log = logger.info if obj.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.log = logger.info if obj.Verbose else logger.debug + + +class SystemExtension(object): + def __init__(self): + self.NameTag = '' + + def addPlaneCoincident(self,d,e1,e2,group=0): + if not group: + group = self.GroupHandle + d = abs(d) + _,p1,n1 = e1 + w2,p2,n2 = e2 + h = [] + if d>0.0: + h.append(self.addPointPlaneDistance(d,p1,w2,group=group)) + h.append(self.addPointsCoincident(p1,p2,w2,group=group)) + else: + h.append(self.addPointsCoincident(p1,p2,group=group)) + h.append(self.addParallel(n1,n2,group=group)) + return h + + def addPlaneAlignment(self,d,e1,e2,group=0): + if not group: + group = self.GroupHandle + d = abs(d) + _,p1,n1 = e1 + w2,_,n2 = e2 + h = [] + if d>0.0: + h.append(self.addPointPlaneDistance(d,p1,w2,group=group)) + else: + h.append(self.addPointInPlane(p1,w2,group=group)) + h.append(self.addParallel(n1,n2,group=group)) + return h + + def addAxialAlignment(self,e1,e2,group=0): + if not group: + group = self.GroupHandle + _,p1,n1 = e1 + w2,p2,n2 = e2 + h = [] + h.append(self.addPointsCoincident(p1,p2,w2,group=group)) + h.append(self.addParallel(n1,n2,group=group)) + return h + + def addMultiParallel(self,e1,e2,group=0): + return self.addParallel(e1,e2,group=group) + + 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 diff --git a/utils.py b/utils.py index a5602ac..c58750b 100644 --- a/utils.py +++ b/utils.py @@ -6,12 +6,32 @@ assembly2 ''' import sys, os - modulePath = os.path.dirname(os.path.realpath(__file__)) + +from PySide.QtCore import Qt +from PySide.QtGui import QIcon, QPainter, QPixmap iconPath = os.path.join(modulePath,'Gui','Resources','icons') +pixmapDisabled = QPixmap(os.path.join(iconPath,'Assembly_Disabled.svg')) +iconSize = (16,16) + +def getIcon(obj,disabled=False,path=None): + if not path: + path = iconPath + if not getattr(obj,'_icon',None): + obj._icon = QIcon(os.path.join(path,obj._iconName)) + if not disabled: + return obj._icon + if not getattr(obj,'_iconDisabled',None): + pixmap = obj._icon.pixmap(*iconSize,mode=QIcon.Disabled) + icon = QIcon(pixmapDisabled) + icon.paint(QPainter(pixmap), + 0,0,iconSize[0],iconSize[1],Qt.AlignCenter) + obj._iconDisabled = QIcon(pixmap) + return obj._iconDisabled + import FreeCAD, FreeCADGui, Part -import numpy +import numpy as np import asm3.FCADLogger logger = asm3.FCADLogger.FCADLogger('assembly3') @@ -34,7 +54,9 @@ def getElement(obj,tp): return obj def isPlanar(obj): - shape = getElement(obj,(Part.Face,Part.Edge)) + if isCircularEdge(obj): + return True + shape = getElement(obj,Part.Face) if not shape: return False elif str(shape.Surface) == '': @@ -91,8 +113,8 @@ def isCircularEdge(obj): except Exception: #FreeCAD exception thrown () return False if all( hasattr(a,'Center') for a in arcs ): - centers = numpy.array([a.Center for a in arcs]) - sigma = numpy.std( centers, axis=0 ) + centers = np.array([a.Center for a in arcs]) + sigma = np.std( centers, axis=0 ) return max(sigma) < 10**-6 return False @@ -114,8 +136,8 @@ def isLinearEdge(obj): return False if all(isLine(a) for a in arcs): lines = arcs - D = numpy.array([L.tangent(0)[0] for L in lines]) #D(irections) - return numpy.std( D, axis=0 ).max() < 10**-9 + D = np.array([L.tangent(0)[0] for L in lines]) #D(irections) + return np.std( D, axis=0 ).max() < 10**-9 return False def isVertex(obj): @@ -158,7 +180,7 @@ def getElementPos(obj): if error_normalized < 10**-6: #then good rotation_axis fix pos = center else: - edge = getElement(obj,'Edge') + edge = getElement(obj,Part.Edge) if edge: if isLine(edge.Curve): pos = edge.Vertexes[-1].Point @@ -168,15 +190,15 @@ def getElementPos(obj): BSpline = edge.Curve.toBSpline() arcs = BSpline.toBiArcs(10**-6) if all( hasattr(a,'Center') for a in arcs ): - centers = numpy.array([a.Center for a in arcs]) - sigma = numpy.std( centers, axis=0 ) + centers = np.array([a.Center for a in arcs]) + sigma = np.std( centers, axis=0 ) if max(sigma) < 10**-6: #then circular curce pos = centers[0] elif all(isLine(a) for a in arcs): lines = arcs - D = numpy.array( + D = np.array( [L.tangent(0)[0] for L in lines]) #D(irections) - if numpy.std( D, axis=0 ).max() < 10**-9: #then linear curve + if np.std( D, axis=0 ).max() < 10**-9: #then linear curve return lines[0].value(0) return pos @@ -214,15 +236,15 @@ def getElementNormal(obj,reverse=False): BSpline = edge.Curve.toBSpline() arcs = BSpline.toBiArcs(10**-6) if all( hasattr(a,'Center') for a in arcs ): - centers = numpy.array([a.Center for a in arcs]) - sigma = numpy.std( centers, axis=0 ) + centers = np.array([a.Center for a in arcs]) + sigma = np.std( centers, axis=0 ) if max(sigma) < 10**-6: #then circular curce axis = arcs[0].Axis if all(isLine(a) for a in arcs): lines = arcs - D = numpy.array( + D = np.array( [L.tangent(0)[0] for L in lines]) #D(irections) - if numpy.std( D, axis=0 ).max() < 10**-9: #then linear curve + if np.std( D, axis=0 ).max() < 10**-9: #then linear curve return D[0] if axis: q = FreeCAD.Rotation(FreeCAD.Vector(0,0,-1 if reverse else 1),axis).Q @@ -253,15 +275,15 @@ def getElementCircular(obj): def fit_plane_to_surface1( surface, n_u=3, n_v=3 ): 'borrowed from assembly2 lib3D.py' - uv = sum( [ [ (u,v) for u in numpy.linspace(0,1,n_u)] - for v in numpy.linspace(0,1,n_v) ], [] ) + uv = sum( [ [ (u,v) for u in np.linspace(0,1,n_u)] + for v in np.linspace(0,1,n_v) ], [] ) # positions at u,v points P = [ surface.value(u,v) for u,v in uv ] - N = [ numpy.cross( *surface.tangent(u,v) ) for u,v in uv ] + N = [ np.cross( *surface.tangent(u,v) ) for u,v in uv ] # plane's normal, averaging done to reduce error plane_norm = sum(N) / len(N) plane_pos = P[0] - error = sum([ abs( numpy.dot(p - plane_pos, plane_norm) ) for p in P ]) + error = sum([ abs( np.dot(p - plane_pos, plane_norm) ) for p in P ]) return plane_norm, plane_pos, error def fit_rotation_axis_to_surface1( surface, n_u=3, n_v=3 ): @@ -270,16 +292,16 @@ def fit_rotation_axis_to_surface1( surface, n_u=3, n_v=3 ): borrowed from assembly2 lib3D.py ''' - uv = sum( [ [ (u,v) for u in numpy.linspace(0,1,n_u)] - for v in numpy.linspace(0,1,n_v) ], [] ) + uv = sum( [ [ (u,v) for u in np.linspace(0,1,n_u)] + for v in np.linspace(0,1,n_v) ], [] ) # positions at u,v points - P = [ numpy.array(surface.value(u,v)) for u,v in uv ] - N = [ numpy.cross( *surface.tangent(u,v) ) for u,v in uv ] + P = [ np.array(surface.value(u,v)) for u,v in uv ] + N = [ np.cross( *surface.tangent(u,v) ) for u,v in uv ] intersections = [] for i in range(len(N)-1): for j in range(i+1,len(N)): # based on the distance_between_axes( p1, u1, p2, u2) function, - if 1 - abs(numpy.dot( N[i], N[j])) < 10**-6: + if 1 - abs(np.dot( N[i], N[j])) < 10**-6: continue #ignore parrallel case p1_x, p1_y, p1_z = P[i] u1_x, u1_y, u1_z = N[i] @@ -293,30 +315,30 @@ def fit_rotation_axis_to_surface1( surface, n_u=3, n_v=3 ): 2*p2_x*u1_x - 2*p2_y*u1_y - 2*p2_z*u1_z t2_coef =-2*p1_x*u2_x - 2*p1_y*u2_y - 2*p1_z*u2_z + \ 2*p2_x*u2_x + 2*p2_y*u2_y + 2*p2_z*u2_z - A = numpy.array([ [ 2*t1_t1_coef , t1_t2_coef ], + A = np.array([ [ 2*t1_t1_coef , t1_t2_coef ], [ t1_t2_coef, 2*t2_t2_coef ] ]) - b = numpy.array([ t1_coef, t2_coef]) + b = np.array([ t1_coef, t2_coef]) try: - t1, t2 = numpy.linalg.solve(A,-b) - except numpy.linalg.LinAlgError: + t1, t2 = np.linalg.solve(A,-b) + except np.linalg.LinAlgError: continue - pos_t1 = P[i] + numpy.array(N[i])*t1 + pos_t1 = P[i] + np.array(N[i])*t1 pos_t2 = P[j] + N[j]*t2 intersections.append( pos_t1 ) intersections.append( pos_t2 ) if len(intersections) < 2: - error = numpy.inf + error = np.inf return 0, 0, error else: # fit vector to intersection points; # http://mathforum.org/library/drmath/view/69103.html - X = numpy.array(intersections) - centroid = numpy.mean(X,axis=0) - M = numpy.array([i - centroid for i in intersections ]) - A = numpy.dot(M.transpose(), M) - # numpy docs: s : (..., K) The singular values for every matrix, + X = np.array(intersections) + centroid = np.mean(X,axis=0) + M = np.array([i - centroid for i in intersections ]) + A = np.dot(M.transpose(), M) + # np docs: s : (..., K) The singular values for every matrix, # sorted in descending order. - _U,s,V = numpy.linalg.svd(A) + _U,s,V = np.linalg.svd(A) axis_pos = centroid axis_dir = V[0] error = s[1] #dont know if this will work @@ -326,5 +348,4 @@ _tol = 10e-7 def isSamePlacement(pla1,pla2): return pla1.Base.distanceToPoint(pla2.Base) < _tol and \ - numpy.linalg.norm(numpy.array(pla1.Rotation.Q) - \ - numpy.array(pla2.Rotation.Q)) < _tol + np.linalg.norm(np.array(pla1.Rotation.Q)-np.array(pla2.Rotation.Q))<_tol