Code refactor to prepare for multiple backend solvers
This commit is contained in:
parent
9402201a5e
commit
59c2c7a35d
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
15
__init__.py
15
__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()
|
||||
|
|
157
assembly.py
157
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)
|
||||
|
||||
|
|
540
constraint.py
540
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)
|
||||
|
|
238
proxy.py
Normal file
238
proxy.py
Normal 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
166
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)
|
||||
|
||||
|
|
45
sys_slvs.py
Normal file
45
sys_slvs.py
Normal 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
164
system.py
Normal 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
|
99
utils.py
99
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) == '<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
|
||||
|
|
Loading…
Reference in New Issue
Block a user