Code refactor to prepare for multiple backend solvers

This commit is contained in:
Zheng, Lei 2017-09-27 04:03:42 +08:00
parent 9402201a5e
commit 59c2c7a35d
9 changed files with 907 additions and 517 deletions

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

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

View File

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

View File

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

238
proxy.py Normal file
View File

@ -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__

166
solver.py
View File

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

45
sys_slvs.py Normal file
View File

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

164
system.py Normal file
View File

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

View File

@ -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) == '<Plane object>':
@ -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