Basic function working

Tested PointsCoincident constraint
This commit is contained in:
Zheng, Lei 2017-09-11 04:31:45 +08:00
parent 402d7a1ef4
commit b5a91ec889
6 changed files with 2089 additions and 0 deletions

66
FCADLogger.py Normal file
View File

@ -0,0 +1,66 @@
import os
from datetime import datetime
import inspect
import FreeCAD, FreeCADGui
class FCADLogger:
def __init__(self, tag, **kargs):
self.tag = tag
self.levels = { 'error':0, 'warn':1, 'info':2,
'debug':3, 'trace':4 }
self.printer = [
FreeCAD.Console.PrintError,
FreeCAD.Console.PrintWarning,
FreeCAD.Console.PrintMessage,
FreeCAD.Console.PrintLog,
FreeCAD.Console.PrintLog ]
self.laststamp = datetime.now()
for key in ('printTag','updateUI','timing','lineno'):
setattr(self,key,kargs.get(key,True))
def _isEnabledFor(self,level):
return FreeCAD.getLogLevel(self.tag) >= level
def isEnabledFor(self,level):
self._isEnabledOf(self.levels[level])
def error(self,msg,frame=0):
self.log(0,msg,frame+1)
def warn(self,msg,frame=0):
self.log(1,msg,frame+1)
def info(self,msg,frame=0):
self.log(2,msg,frame+1)
def debug(self,msg,frame=0):
self.log(3,msg,frame+1)
def trace(self,msg,frame=0):
self.log(4,msg,frame+1)
def log(self,level,msg,frame=0):
if not self._isEnabledFor(level):
return
prefix = ''
if self.printTag:
prefix += '<{}> '.format(self.tag)
if self.timing:
now = datetime.now()
prefix += '{} - '.format((now-self.laststamp).total_seconds())
self.laststamp = now
if self.lineno:
stack = inspect.stack()[frame+1]
prefix += '{}({}): '.format(os.path.basename(stack[1]),stack[2])
self.printer[level]('{}{}\n'.format(prefix,msg))
if self.updateUI:
try:
FreeCADGui.updateGui()
except Exception:
pass

26
__init__.py Normal file
View File

@ -0,0 +1,26 @@
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.assembly import Assembly,AsmConstraint
def test():
doc = FreeCAD.newDocument()
cylinder1 = doc.addObject('Part::Cylinder','cylinder1')
cylinder1.Visibility = False
asm1 = Assembly.make(doc)
asm1.Proxy.getPartGroup().setLink({-1:cylinder1})
cylinder2 = doc.addObject('Part::Cylinder','cylinder2')
cylinder2.Visibility = False
asm2 = Assembly.make(doc)
asm2.Placement.Base.z = -20
asm2.Proxy.getPartGroup().setLink({-1:cylinder2})
doc.recompute()
FreeCADGui.SendMsgToActiveView("ViewFit")
asm = Assembly.make(doc)
asm.Proxy.getPartGroup().setLink((asm1,asm2))
asm1.Visibility = False
asm2.Visibility = False

982
assembly.py Normal file
View File

@ -0,0 +1,982 @@
import sys, os
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
from collections import namedtuple
import FreeCAD, FreeCADGui
import asm3.constraint as constraint
from asm3.utils import logger, objName
def setupUndo(doc,undoDocs,name='Assembly3 solve'):
if doc in undoDocs:
return
doc.openTransaction(name)
undoDocs.add(doc)
def isTypeOf(obj,tp,resolve=False):
if not obj:
return False
if not tp:
return True
if resolve:
obj = obj.getLinkedObject(True)
return isinstance(getattr(obj,'Proxy',None),tp)
def checkType(obj,tp,resolve=False):
if not isTypeOf(obj,tp,resolve):
raise TypeError('Expect object "{}" to be of type "{}"'.format(
objName(obj),tp.__name__))
def getProxy(obj,tp):
checkType(obj,tp)
return obj.Proxy
class AsmBase(object):
def __init__(self):
self.obj = None
def __getstate__(self):
return
def __setstate__(self,_state):
return
def attach(self,obj):
obj.addExtension('App::LinkBaseExtensionPython', None)
self.linkSetup(obj)
def linkSetup(self,obj):
assert getattr(obj,'Proxy',None)==self
self.obj = obj
return
def getViewProviderName(self,_obj):
return 'Gui::ViewProviderLinkPython'
def onDocumentRestored(self, obj):
self.linkSetup(obj)
def onChanged(self,_obj,_prop):
pass
class ViewProviderAsmBase(object):
def __init__(self,vobj):
vobj.Visibility = False
vobj.Proxy = self
self.attach(vobj)
def attach(self,vobj):
self.ViewObject = vobj
def __getstate__(self):
return None
def __setstate__(self, _state):
return None
class AsmGroup(AsmBase):
def linkSetup(self,obj):
super(AsmGroup,self).linkSetup(obj)
obj.configLinkProperty(
'VisibilityList',LinkMode='GroupMode',ElementList='Group')
self.setGroupMode()
def setGroupMode(self):
self.obj.GroupMode = 1 # auto delete children
self.obj.setPropertyStatus('GroupMode','Hidden')
self.obj.setPropertyStatus('GroupMode','Immutable')
self.obj.setPropertyStatus('GroupMode','Transient')
def attach(self,obj):
obj.addProperty("App::PropertyLinkList","Group","Base",'')
obj.addProperty("App::PropertyBoolList","VisibilityList","Base",'')
obj.addProperty("App::PropertyEnumeration","GroupMode","Base",'')
super(AsmGroup,self).attach(obj)
class ViewProviderAsmGroup(ViewProviderAsmBase):
def claimChildren(self):
return self.ViewObject.Object.Group
class AsmPartGroup(AsmGroup):
def __init__(self,parent):
self.parent = getProxy(parent,Assembly)
super(AsmPartGroup,self).__init__()
def setGroupMode(self):
pass
@staticmethod
def make(parent,name='Parts'):
obj = parent.Document.addObject("App::FeaturePython",name,
AsmPartGroup(parent),None,True)
ViewProviderAsmPartGroup(obj.ViewObject)
return obj
class ViewProviderAsmPartGroup(ViewProviderAsmBase):
def onDelete(self,_obj,_subs):
return False
class AsmElement(AsmBase):
def __init__(self,parent):
self.shape = None
self.parent = getProxy(parent,AsmElementGroup)
super(AsmElement,self).__init__()
def linkSetup(self,obj):
super(AsmElement,self).linkSetup(obj)
obj.configLinkProperty('LinkedObject')
obj.setPropertyStatus('LinkedObject','Immutable')
obj.setPropertyStatus('LinkedObject','ReadOnly')
def attach(self,obj):
obj.addProperty("App::PropertyXLink","LinkedObject"," Link",'')
super(AsmElement,self).attach(obj)
def execute(self,_obj):
self.getShape(True)
return False
def getShape(self,refresh=False):
if not refresh:
ret = getattr(self,'shape',None)
if ret:
return ret
self.shape = None
self.shape = self.obj.getSubObject('')
return self.shape
def getAssembly(self):
return self.parent.parent
def getSubElement(self):
link = self.obj.LinkedObject
if isinstance(link,tuple):
return link[1].split('.')[-1]
return ''
def getSubName(self):
link = self.obj.LinkedObject
if not isinstance(link,tuple):
raise RuntimeError('Invalid element link "{}"'.format(
objName(self.obj)))
return link[1]
def setLink(self,owner,subname):
# subname must be relative to the part group object of the parent
# assembly
# check old linked object for auto re-label
obj = self.obj
linked = obj.getLinkedObject(False)
if linked and linked!=obj:
label = linked.Label + '_' + self.getSubElement()
else:
label = ''
obj.setLink(owner,subname)
if obj.Label==obj.Name or obj.Label==label:
linked = obj.getLinkedObject(False)
if linked and linked!=obj:
obj.Label = linked.Label+'_'+self.getSubElement()
else:
obj.Label = obj.Name
Selection = namedtuple('AsmElementSelection',
('Assembly','Element','Subname'))
@staticmethod
def getSelection():
'''
Parse Gui.Selection for making a element
If there is only one selection, then the selection must refer to a sub
element of some part object of an assembly. We shall create a new
element beloning to the top-level assembly
If there are two selections, then first one shall be either the
element group or an individual element. The second selection shall
be a sub element belong to a child assembly of the parent assembly of
the first selected element/element group
'''
sels = FreeCADGui.Selection.getSelectionEx('',False)
if not sels:
return
if len(sels)>1:
raise RuntimeError(
'The selections must have a common (grand)parent assembly')
sel = sels[0]
subs = sel.SubElementNames
if len(subs)>2:
raise RuntimeError('At most two selection is allowed.\n'
'The first selection must be a sub element belonging to some '
'assembly. The optional second selection must be an element '
'belonging to the same assembly of the first selection')
subElement = subs[0].split('.')[-1]
if not subElement:
raise RuntimeError(
'Please select a sub element belonging to some assembly')
link = Assembly.findPartGroup(sel.Object,subs[0])
if not link:
raise RuntimeError(
'Selected sub element does not belong to an assembly')
element = None
if len(subs)>1:
ret = Assembly.findElementGroup(sel.Object,subs[1])
if not ret:
raise RuntimeError('The second selection must be an element')
if ret.Assembly != link.Assembly:
raise RuntimeError(
'The two selections must belong to the same assembly')
element = ret.Object.getSubObject(ret.Subname,1)
if not isTypeOf(element,AsmElement):
raise RuntimeError('The second selection must be an element')
return AsmElement.Selection(
link.Assembly,element,link.Subname+subElement)
@staticmethod
def make(selection=None,name='Element'):
if not selection:
selection = AsmElement.getSelection()
assembly = getProxy(selection.Assembly,Assembly)
element = selection.Element
if not element:
elements = assembly.getElementGroup()
# try to search the element group for an existing element
for e in elements.Group:
if getProxy(e,AsmElement).getSubName() == selection.Subname:
return element
element = elements.Document.addObject("App::FeaturePython",
name,AsmElement(elements),None,True)
ViewProviderAsmElement(element.ViewObject)
elements.setLink({-1:element})
getProxy(element,AsmElement).setLink(
assembly.getPartGroup(),selection.Subname)
return element
class ViewProviderAsmElement(ViewProviderAsmBase):
def attach(self,vobj):
super(ViewProviderAsmElement,self).attach(vobj)
vobj.OverrideMaterial = True
vobj.ShapeMaterial.DiffuseColor = self.getDefaultColor()
vobj.ShapeMaterial.EmissiveColor = self.getDefaultColor()
vobj.DrawStyle = 1
vobj.LineWidth = 4
vobj.PointSize = 6
def getDefaultColor(self):
return (60.0/255.0,1.0,1.0)
class AsmElementLink(AsmBase):
def __init__(self,parent):
super(AsmElementLink,self).__init__()
self.info = None
self.parent = getProxy(parent,AsmConstraint)
def linkSetup(self,obj):
super(AsmElementLink,self).linkSetup(obj)
obj.configLinkProperty('LinkedObject')
obj.setPropertyStatus('LinkedObject','Immutable')
obj.setPropertyStatus('LinkedObject','ReadOnly')
def attach(self,obj):
obj.addProperty("App::PropertyXLink","LinkedObject"," Link",'')
super(AsmElementLink,self).attach(obj)
def execute(self,_obj):
self.getInfo(True)
return False
def getAssembly(self):
return self.parent.parent.parent
def getElement(self):
linked = self.obj.getLinkedObject(False)
if not linked:
raise RuntimeError('Element link broken')
if not isTypeOf(linked,AsmElement):
raise RuntimeError('Invalid element type')
return linked.Proxy
def getSubName(self):
link = self.obj.LinkedObject
if not isinstance(link,tuple):
raise RuntimeError('Invalid element link "{}"'.format(
objName(self.obj)))
return link[1]
def getShapeSubName(self):
element = self.getElement()
assembly = element.getAssembly()
if assembly == self.getAssembly():
return element.getSubName()
# pop two names from back (i.e. element group, element)
subname = self.getSubName()
sub = subname.split('.')[:-3]
sub = '.'.join(sub) + '.' + assembly.getPartGroup().Name + \
'.' + element.getSubName()
logger.debug('shape subname {} -> {}'.format(subname,sub))
return sub
def prepareLink(self,owner,subname):
assembly = self.getAssembly()
sobj = owner.getSubObject(subname,1)
if not sobj:
raise RuntimeError('invalid element link {} broken: {}'.format(
objName(owner),subname))
if isTypeOf(sobj,AsmElementLink):
# if it points to another AsElementLink that belongs the same
# assembly, simply return the same link
if sobj.Proxy.getAssembly() == assembly:
return (owner,subname)
# If it is from another assembly (i.e. a nested assembly), convert
# the subname reference by poping three names (constraint group,
# constraint, element link) from the back, and then append with the
# element link's own subname reference
sub = subname.split('.')[:-4]
sub = '.'.join(subname)+'.'+sobj.Proxy.getSubName()
logger.debug('convert element link {} -> {}'.format(subname,sub))
return (owner,sub)
if isTypeOf(sobj,AsmElement):
return (owner,subname)
# try to see if the reference comes from some nested assembly
ret = assembly.findChild(owner,subname,recursive=True)
if not ret:
# It is from a non assembly child part, then use our own element
# group as the holder for elements
ret = [Assembly.Selection(assembly.obj,owner,subname)]
if not isTypeOf(ret[-1].Object,AsmPartGroup):
raise RuntimeError('Invalid element link ' + subname)
# call AsmElement.make to either create a new element, or an existing
# element if there is one
element = AsmElement.make(AsmElement.Selection(
ret[-1].Assembly,None,ret[-1].Subname))
if ret[-1].Assembly == assembly.obj:
return (assembly.getElementGroup(),element.Name+'.')
elementSub = ret[-1].Object.Name + '.' + ret[-1].Subname
sub = subname[:-(len(elementSub)+1)] + '.' + \
ret[-1].Assembly.Proxy.getElementGroup().Name + '.' + \
element.Name + '.'
logger.debug('generate new element {} -> {}'.format(subname,sub))
return (owner,sub)
def setLink(self,owner,subname):
obj = self.obj
obj.setLink(*self.prepareLink(owner,subname))
linked = obj.getLinkedObject(False)
if linked and linked!=obj:
obj.Label = 'Link_'+linked.Label
else:
obj.Label = obj.Name
Info = namedtuple('AsmElementLinkInfo',
('Part','PartName','Placement','Object','Subname','Shape'))
def getInfo(self,refresh=False):
if not refresh:
ret = getattr(self,'info',None)
if ret:
return ret
self.info = None
assembly = self.getAssembly()
subname = self.getShapeSubName()
names = subname.split('.')
partGroup = assembly.getPartGroup()
part = partGroup.getSubObject(names[0]+'.',1)
if not part:
raise RuntimeError('Eelement link "{}" borken: {}'.format(
objName(self.obj),subname))
# For storing the shape of the element with proper transformation
shape = None
# For storing the placement of the movable part
pla = None
# For storing the actual geometry object of the part, in case 'part' is
# a link
obj = None
if not isTypeOf(part,Assembly,True) and part!=partGroup.Group[0]:
getter = getattr(part.getLinkedObject(True),'getLinkExtProperty')
# special treatment of link array (i.e. when ElementCount!=0), we
# allow the array element to be moveable by the solver
if getter and getter('ElementCount'):
# store both the part (i.e. the link array), and the array
# element object
part = (part,part.getSubObject(names[1]+'.',1))
# trim the subname to be after the array element
sub = '.'.join(names[2:])
# There are two states of an link array.
if getter('ElementList'):
# a) The elements are expanded as individual objects, i.e
# when ElementList has members, then the moveable Placement
# is a property of the array element. So we obtain the shape
# before 'Placement' by setting 'transform' set to False.
shape=part[1].getSubObject(sub,transform=False)
pla = part[1].Placement
obj = part[0].getLinkedObject(False)
partName = part[1].Name
else:
# b) The elements are collapsed. Then the moveable Placement
# is stored inside link object's PlacementList property. So,
# the shape obtained below is already before 'Placement',
# i.e. no need to set 'transform' to False.
shape=part[1].getSubObject(sub)
obj = part[1]
try:
idx = names[1].split('_i')[-1]
# we store the array index instead, in order to modified
# Placement later when the solver is done. Also because
# that when the elements are collapsed, there is really
# no element object here.
part = (part[0],int(idx),part[1])
pla = part[0].PlacementList[idx]
except ValueError:
raise RuntimeError('invalid array subname of element '
'"{}": {}'.format(objName(self.obj),subname))
partName = '{}.{}.'.format(part[0].Name,idx)
subname = sub
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
# 'transform' to False
subname = '.'.join(names[1:])
shape = part.getSubObject(subname,transform=False)
pla = part.Placement
obj = part.getLinkedObject(False)
partName = part.Name
self.info = AsmElementLink.Info(
part,partName,pla.copy(),obj,subname,shape.copy())
return self.info
@staticmethod
def setPlacement(part,pla,undoDocs):
'''
called by solver after solving to adjust the placement.
part: obtained by AsmConstraint.getInfo().Part
pla: the new placement
'''
if isinstance(part,tuple):
if isinstance(part[1],int):
setupUndo(part[0].Document,undoDocs)
part[0].PlacementList = {part[1]:pla}
else:
setupUndo(part[1].Document,undoDocs)
part[1].Placement = pla
else:
setupUndo(part.Document,undoDocs)
part.Placement = pla
MakeInfo = namedtuple('AsmElementLinkSelection',
('Constraint','Owner','Subname'))
@staticmethod
def make(info,name='ElementLink'):
element = info.Constraint.Document.addObject("App::FeaturePython",
name,AsmElementLink(info.Constraint),None,True)
ViewProviderAsmElementLink(element.ViewObject)
info.Constraint.setLink({-1:element})
element.Proxy.setLink(info.Owner,info.Subname)
return element
class ViewProviderAsmElementLink(ViewProviderAsmBase):
pass
class AsmConstraint(AsmGroup):
def __init__(self,parent):
self.elements = None
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 solve 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)
super(AsmConstraint,self).attach(obj)
def onChanged(self,obj,prop):
constraint.onChanged(obj,prop)
super(AsmConstraint,self).onChanged(obj,prop)
def linkSetup(self,obj):
self.elements = None
super(AsmConstraint,self).linkSetup(obj)
obj.setPropertyStatus('VisibilityList','Output')
for o in obj.Group:
getProxy(o,AsmElementLink).parent = self
constraint.attach(obj)
def execute(self,_obj):
self.getElements(True)
return False
def getElements(self,refresh=False):
if refresh:
self.elements = None
ret = getattr(self,'elements',None)
obj = self.obj
if ret or not obj._Type:
return ret
shapes = []
elements = []
for o in obj.Group:
checkType(o,AsmElementLink)
info = o.Proxy.getInfo()
shapes.append(info.Shape)
elements.append(o)
constraint.check(obj._Type,shapes)
self.elements = elements
return self.elements
Selection = namedtuple('ConstraintSelection',
('Assembly','Constraint','Elements'))
@staticmethod
def getSelection(tp=0):
'''
Parse Gui.Selection for making a constraint
The selected elements must all belong to the same immediate parent
assembly.
'''
sels = FreeCADGui.Selection.getSelectionEx('',False)
if not sels:
return
if len(sels)>1:
raise RuntimeError(
'The selections must have a common (grand)parent assembly')
sel = sels[0]
cstr = None
elements = []
assembly = None
for sub in sel.SubElementNames:
sobj = sel.Object.getSubObject(sub,1)
ret = Assembly.findChild(sel.Object,sub,recursive=True)
if not ret:
raise RuntimeError('Selection {}.{} is not from an '
'assembly'.format(sel.Object.Name,sub))
if not assembly:
# check if the selection is a constraint group or a constraint
if isTypeOf(sobj,AsmConstraintGroup):
assembly = ret[-1].Assembly
continue
if isTypeOf(sobj,AsmConstraint):
cstr = sobj
assembly = ret[-1].Assembly
continue
assembly = ret[0].Assembly
found = None
for r in ret:
if r.Assembly == assembly:
found = r
break
if not found:
raise RuntimeError('Selection {}.{} is not from the target '
'assembly {}'.format(sel.Object.Name,sub,objName(assembly)))
elements.append((found.Object,found.Subname))
check = None
if cstr and cstr._Type:
tp = cstr._Type
info = cstr.Proxy.getInfo()
check = [o.getShape() for o in info.Elements] + elements
elif tp:
check = elements
if check:
constraint.check(tp,check)
return AsmConstraint.Selection(assembly,cstr,elements)
@staticmethod
def make(tp=0, selection=None, name='Constraint'):
if not selection:
selection = AsmConstraint.getSelection(tp)
if selection.Constraint:
cstr = selection.Constraint
else:
constraints = selection.Assembly.Proxy.getConstraintGroup()
cstr = constraints.Document.addObject("App::FeaturePython",
name,AsmConstraint(constraints),None,True)
ViewProviderAsmConstraint(cstr.ViewObject)
constraints.setLink({-1:cstr})
cstr._Type = tp
for e in selection.Elements:
AsmElementLink.make(AsmElementLink.MakeInfo(cstr,*e))
return cstr
class ViewProviderAsmConstraint(ViewProviderAsmGroup):
def attach(self,vobj):
super(ViewProviderAsmConstraint,self).attach(vobj)
vobj.OverrideMaterial = True
vobj.ShapeMaterial.DiffuseColor = self.getDefaultColor()
vobj.ShapeMaterial.EmissiveColor = self.getDefaultColor()
def getDefaultColor(self):
return (1.0,60.0/255.0,60.0/255.0)
class AsmConstraintGroup(AsmGroup):
def __init__(self,parent):
self.parent = getProxy(parent,Assembly)
super(AsmConstraintGroup,self).__init__()
def linkSetup(self,obj):
super(AsmConstraintGroup,self).linkSetup(obj)
obj.setPropertyStatus('VisibilityList','Output')
for o in obj.Group:
getProxy(o,AsmConstraint).parent = self
@staticmethod
def make(parent,name='Constraints'):
obj = parent.Document.addObject("App::FeaturePython",name,
AsmConstraintGroup(parent),None,True)
ViewProviderAsmConstraintGroup(obj.ViewObject)
return obj
class ViewProviderAsmConstraintGroup(ViewProviderAsmBase):
pass
class AsmElementGroup(AsmGroup):
def __init__(self,parent):
self.parent = getProxy(parent,Assembly)
super(AsmElementGroup,self).__init__()
def linkSetup(self,obj):
super(AsmElementGroup,self).linkSetup(obj)
obj.setPropertyStatus('VisibilityList','Output')
for o in obj.Group:
getProxy(o,AsmElement).parent = self
@staticmethod
def make(parent,name='Elements'):
obj = parent.Document.addObject("App::FeaturePython",name,
AsmElementGroup(parent),None,True)
ViewProviderAsmElementGroup(obj.ViewObject)
return obj
class ViewProviderAsmElementGroup(ViewProviderAsmBase):
def onDelete(self,_obj,_subs):
return False
def canDragObject(self,_obj):
return False
def canDragObjects(self):
return False
def canDragAndDropObject(self,_obj):
return False
def canDropObjectEx(self,_obj,owner,subname):
# check if is dropping a sub-element
if subname.rfind('.')+1 == len(subname):
return False
return self.ViewObject.Object.Proxy.parent.getPartGroup()==owner
def dropObjectEx(self,vobj,_obj,_owner,subname):
AsmElement.make(AsmElement.Selection(
vobj.Object.Proxy.parent.obj,None,subname))
BuildShapeNames = ('No','Compound','Fuse','Cut')
BuildShapeEnum = namedtuple('AsmBuildShapeEnum',BuildShapeNames)(
*range(len(BuildShapeNames)))
class Assembly(AsmGroup):
def __init__(self):
self.constraints = None
super(Assembly,self).__init__()
def execute(self,_obj):
self.constraints = None
self.buildShape()
return False # return False to call LinkBaseExtension::execute()
def buildShape(self):
obj = self.obj
if not obj.BuildShape:
obj.Shape.nullify()
return
import Part
shape = []
partGroup = self.getPartGroup(obj)
group = partGroup.Group
if not group:
raise RuntimeError('no parts')
if obj.BuildShape == BuildShapeEnum.Cut:
shape = Part.getShape(group[0]).Solids
if not shape:
raise RuntimeError('First part has no solid')
if len(shape)>1:
shape = [shape[0].fuse(shape[1:])]
group = group[1:]
for o in group:
if obj.isElementVisible(o.Name):
shape += Part.getShape(o).Solids
if not shape:
raise RuntimeError('No solids found in parts')
if len(shape) == 1:
obj.Shape = shape[0]
elif obj.BuildShape == BuildShapeEnum.Fuse:
obj.Shape = shape[0].fuse(shape[1:])
elif obj.BuildShape == BuildShapeEnum.Cut:
if len(shape)>2:
obj.Shape = shape[0].cut(shape[1].fuse(shape[2:]))
else:
obj.Shape = shape[0].cut(shape[1])
else:
obj.Shape = Part.makeCompound(shape)
def attach(self, obj):
obj.addProperty("App::PropertyEnumeration","BuildShape","Base",'')
obj.BuildShape = BuildShapeNames
super(Assembly,self).attach(obj)
def linkSetup(self,obj):
obj.configLinkProperty('Placement')
super(Assembly,self).linkSetup(obj)
self.onChanged(obj,'BuildShape')
# make sure all children are there, first constraint group, then element
# group, and finally part group. Call getPartGroup below will make sure
# all groups exist. The order of the group is important to make sure
# correct rendering and picking behavior
self.getPartGroup(True)
def onChanged(self, obj, prop):
if prop == 'BuildShape':
if not obj.BuildShape or obj.BuildShape == BuildShapeEnum.Compound:
obj.setPropertyStatus('Shape','-Transient')
else:
obj.setPropertyStatus('Shape','Transient')
def getConstraintGroup(self, create=False):
obj = self.obj
try:
ret = obj.Group[0]
checkType(ret,AsmConstraintGroup)
parent = getattr(ret.Proxy,'parent',None)
if not parent:
ret.Proxy.parent = self
elif parent!=self:
raise RuntimeError('invalid parent of constraint group '
'{}'.format(objName(ret)))
return ret
except IndexError:
if not create:
return # constraint group is optional, so, no exception
if obj.Group:
raise RuntimeError('Invalid assembly')
ret = AsmConstraintGroup.make(obj)
obj.setLink({0:ret})
return ret
def getConstraints(self,refresh=False):
if not refresh:
ret = getattr(self,'constraints',None)
if ret:
return ret
self.constraints = None
cstrGroup = self.getConstraintGroup()
if not cstrGroup:
return
ret = []
for o in cstrGroup.Group:
checkType(o,AsmConstraint)
if not o._Type:
logger.debug('skip constraint "{}" type '
'"{}"'.format(objName(o),o.Type))
continue
ret.append(o)
self.constraints = ret
return self.constraints
def getElementGroup(self,create=False):
obj = self.obj
try:
ret = obj.Group[1]
checkType(ret,AsmElementGroup)
parent = getattr(ret.Proxy,'parent',None)
if not parent:
ret.Proxy.parent = self
elif parent!=self:
raise RuntimeError('invalid parent of element group '
'{}'.format(objName(ret)))
return ret
except IndexError:
if not create:
raise RuntimeError('Missing element group')
self.getConstraintGroup(True)
ret = AsmElementGroup.make(obj)
obj.setLink({1:ret})
return ret
def getPartGroup(self,create=False):
obj = self.obj
try:
ret = obj.Group[2]
checkType(ret,AsmPartGroup)
parent = getattr(ret.Proxy,'parent',None)
if not parent:
ret.Proxy.parent = self
elif parent!=self:
raise RuntimeError(
'invalid parent of part group {}'.format(objName(ret)))
return ret
except IndexError:
if not create:
raise RuntimeError('Missing part group')
self.getConstraintGroup(True)
self.getElementGroup(True)
ret = AsmPartGroup.make(obj)
obj.setLink({2:ret})
return ret
@staticmethod
def make(doc=None,name='Assembly'):
if not doc:
doc = FreeCAD.ActiveDocument
obj = doc.addObject(
"Part::FeaturePython",name,Assembly(),None,True)
ViewProviderAssembly(obj.ViewObject)
obj.Visibility = True
return obj
Info = namedtuple('AssemblyInfo',('Assembly','Object','Subname'))
@staticmethod
def findChild(obj,subname,childType=None,
recursive=False,relativeToChild=True):
'''
Find the immediate child of the first Assembly referenced in 'subs'
obj: the parent object
subname: '.' separted sub-object reference, or string list of sub-object
names. Must contain no sub element name.
childType: optional checking of the child type.
recursive: If True, continue finding the child of the next assembly.
relativeToChild: If True, the returned subname is realtive to the child
object found, or else, it is relative to the assembly, i.e., including
the child's name
Return None if not found, or (assembly,child,sub), where 'sub' is the
remaining sub name list. If recursive is True, then return a list of
tuples
'''
assembly = None
child = None
idx = -1
if isTypeOf(obj,Assembly,True):
assembly = obj
subs = subname if isinstance(subname,list) else subname.split('.')
for i,name in enumerate(subs[:-2]):
obj = obj.getSubObject(name+'.',1)
if not obj:
raise RuntimeError('Cannot find sub object {}'.format(name))
if assembly and isTypeOf(obj,childType):
child = obj
if relativeToChild:
idx = i+1
else:
idx = i
break
assembly = obj if isTypeOf(obj,Assembly,True) else None
if not child:
return
subs = subs[idx:]
ret = Assembly.Info(assembly,child,'.'.join(subs))
if not recursive:
return ret
nret = Assembly.findChild(child,subs,childType,True)
if nret:
return [ret] + nret
return [ret]
@staticmethod
def findPartGroup(obj,subname='2.',recursive=False,relativeToChild=True):
return Assembly.findChild(
obj,subname,AsmPartGroup,recursive,relativeToChild)
@staticmethod
def findElementGroup(obj,subname='1.',relativeToChild=True):
return Assembly.findChild(
obj,subname,AsmElementGroup,False,relativeToChild)
@staticmethod
def findConstraintGroup(obj,subname='0.',relativeToChild=True):
return Assembly.findChild(
obj,subname,AsmConstraintGroup,False,relativeToChild)
class ViewProviderAssembly(ViewProviderAsmGroup):
def canDragObject(self,_child):
return False
def canDragObjects(self):
return False
def canDropObject(self,_child):
return False
def canDropObjects(self):
return False

521
constraint.py Normal file
View File

@ -0,0 +1,521 @@
from collections import namedtuple
import FreeCAD, FreeCADGui
import asm3.utils as utils
import asm3.slvs as slvs
from asm3.utils import logger, objName
Types = []
TypeMap = {}
TypeNameMap = {}
class ConstraintType(type):
def __init__(cls, name, bases, attrs):
super(ConstraintType,cls).__init__(name,bases,attrs)
if cls._id >= 0:
if cls._id in TypeMap:
raise RuntimeError(
'Duplicate constriant type id {}'.format(cls._id))
if cls.slvsFunc():
TypeMap[cls._id] = cls
TypeNameMap[cls.getName()] = cls
cls._idx = len(Types)
logger.debug('register constraint "{}":{},{}'.format(
cls.getName(),cls._id,cls._idx))
Types.append(cls)
# PartName: text name of the part
# Placement: the original placement of the part
# Params: 7 parameters that defines the transformation
# Workplane: a tuple of three entity handles, that is the workplane, the origin
# point, and the normal. The workplane, defined by the origin and
# norml, is essentially the XY reference plane of the part.
# EntityMap: string -> entity handle map, for caching
PartInfo = namedtuple('SolverPartInfo',
('PartName','Placement','Params','Workplane','EntityMap'))
def _addEntity(etype,system,partInfo,key,shape):
key += '.{}'.format(etype)
h = partInfo.EntityMap.get(key,None)
if h:
logger.debug('cache {}: {}'.format(key,h))
return h
if etype == 'p': # point
v = utils.getElementPos(shape)
e = system.addPoint3dV(*v)
elif etype == 'n': # normal
v = utils.getElementNormal(shape)
e = system.addNormal3dV(*v)
else:
raise RuntimeError('unknown entity type {}'.format(etype))
h = system.addTransform(e,*partInfo.Params)
logger.debug('{}: {},{}, {}'.format(key,h,e,v))
partInfo.EntityMap[key] = h
return h
def _p(system,partInfo,key,shape):
'return a slvs handle of a transformed point derived from "shape"'
if not system:
if utils.hasCenter(shape):
return
return 'a vertex or circular edge/face'
return _addEntity('p',system,partInfo,key,shape)
def _n(system,partInfo,key,shape):
'return a slvs handle of a transformed normal derived from "shape"'
if not system:
if utils.isAxisOfPlane(shape):
return
return 'an edge or face with a surface normal'
return _addEntity('n',system,partInfo,key,shape)
def _l(system,partInfo,key,shape,retAll=False):
'return a pair of slvs handle of the end points of an edge in "shape"'
if not system:
if utils.isLinearEdge(shape):
return
return 'a linear edge'
key += '.l'
h = partInfo.EntityMap.get(key,None)
if h:
logger.debug('cache {}: {}'.format(key,h))
else:
v = shape.Edges[0].Vertexes
p1 = system.addPoint3dV(*v[0].Point)
p2 = system.addPoint3dV(*v[-1].Point)
h = system.addLine(p1,p2)
h = (h,p1,p2)
logger.debug('{}: {}'.format(key,h))
partInfo.EntityMap[key] = h
return h if retAll else h[0]
def _w(system,partInfo,key,shape,retAll=False):
'return a slvs handle of a transformed plane/workplane from "shape"'
if not system:
if utils.isAxisOfPlane(shape):
return
return 'an edge or face with a planar surface'
key2 = key+'.w'
h = partInfo.EntityMap.get(key2,None)
if h:
logger.debug('cache {}: {}'.format(key,h))
else:
p = _p(system,partInfo,key,shape)
n = _n(system,partInfo,key,shape)
h = system.addWorkplane(p,n)
h = (h,p,n)
logger.debug('{}: {}'.format(key,h))
partInfo.EntityMap[key2] = h
return h if retAll else h[0]
def _c(system,partInfo,key,shape,requireArc=False):
'return a slvs handle of a transformed circle/arc derived from "shape"'
if not system:
r = utils.getElementCircular(shape)
if not r or (requireArc and not isinstance(r,list,tuple)):
return
return 'an cicular arc edge' if requireArc else 'a circular edge'
key2 = key+'.c'
h = partInfo.EntityMap.get(key2,None)
if h:
logger.debug('cache {}: {}'.format(key,h))
else:
h = _w(system,partInfo,key,shape,True)
r = utils.getElementCircular(shape)
if not r:
raise RuntimeError('shape is not cicular')
if isinstance(r,(list,tuple)):
l = _l(system,partInfo,key,shape,True)
h += l[1:]
h = system.addArcOfCircleV(*h)
elif requireArc:
raise RuntimeError('shape is not an arc')
else:
h = h[1:]
h.append(system.addDistanceV(r))
h = system.addCircle(*h)
logger.debug('{}: {}, {}'.format(key,h,r))
partInfo.EntityMap[key2] = h
return h
def _a(system,partInfo,key,shape):
return _c(system,partInfo,key,shape,True)
_PropertyDistance = ('Value','Distance','PropertyDistance','Constraint')
_PropertyAngle = ('Value','Angle','PropertyAngle','Constraint')
_PropertyRatio = (None,'Ratio','PropertyFloat','Constraint')
_PropertyDifference = (None,'Difference','PropertyFloat','Constraint')
_PropertyDiameter = (None,'Diameter','PropertyFloat','Constraint')
_PropertyRadius = (None,'Radius','PropertyFloat','Constraint')
_PropertySupplement = (None,'Supplement','PropertyBool','Constraint',
'If True, then the second angle is calculated as 180-angle')
_PropertyAtEnd = (None,'AtEnd','PropertyBool','Constraint',
'If True, then tangent at the end point, or else at the start point')
_ordinal = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th' ]
class Base:
__metaclass__ = ConstraintType
_id = -1
_entities = []
_workplane = False
_props = []
_func = None
@classmethod
def getName(cls):
return cls.__name__
@classmethod
def slvsFunc(cls):
try:
if not cls._func:
cls._func = getattr(slvs.System,'add'+cls.getName())
return cls._func
except AttributeError:
logger.error('Invalid slvs constraint "{}"'.format(cls.getName()))
@classmethod
def getEntityDef(cls,group,checkCount,name=None):
entities = cls._entities
if len(group) != len(entities):
if not checkCount and len(group)<len(entities):
return entities[:len(group)]
if cls._workplane and len(group)==len(entities)+1:
entities = list(entities)
entities.append(_w)
else:
if not name:
name = cls.getName()
else:
name += ' of type "{}"'.format(cls.getName)
raise RuntimeError('Constraint {} has wrong number of '
'elements {}, expecting {}'.format(
name,len(group),len(entities)))
return entities
@classmethod
def check(cls,group):
entities = cls.getEntityDef(group,False)
for i,e in enumerate(entities):
o = group[i]
msg = e(None,None,None,o)
if not msg:
continue
if i == len(cls._entities):
raise RuntimeError('Constraint {} requires the optional {} '
'element to be a planar face for defining a '
'workplane'.format(cls.getName(), _ordinal[i], msg))
raise RuntimeError('Constraint {} requires the {} element to be'
' {}'.format(cls.getName(), _ordinal[i], msg))
def __init__(self,obj,_props):
if obj._Type != self._id:
if self._id < 0:
raise RuntimeError('invalid constraint type {} id: '
'{}'.format(self.__class__,self._id))
obj._Type = self._id
for prop in self.__class__._props:
obj.addProperty(*prop[1:])
@classmethod
def detach(cls,obj):
for prop in cls._props:
obj.removeProperty(prop[1])
def onChanged(self,obj,prop):
pass
@classmethod
def getEntities(cls,obj,solver):
'''maps fcad element shape to slvs entities'''
ret = []
for prop in cls._props:
v = getattr(obj,prop[1])
if prop[0]:
v = getattr(v,prop[0])()
ret.append(v)
elements = obj.Proxy.getElements()
entities = cls.getEntityDef(elements,True,objName(obj))
ret = []
for e,o in zip(entities,elements):
info = o.Proxy.getInfo()
partInfo = solver.getPartInfo(info)
ret.append(e(solver.system,partInfo,info.Subname,info.Shape))
logger.debug('{}: {}, {}'.format(objName(obj),obj.Type,ret))
return ret
@classmethod
def prepare(cls,obj,solver):
e = cls.getEntities(obj,solver)
cls._func(solver.system,*e,group=solver.group)
class Disabled(Base):
_id = 0
_func = True
@classmethod
def prepare(cls,_obj,_solver):
pass
class PointsCoincident(Base):
_id = 1
_entities = (_p,_p)
_workplane = True
class SameOrientation(Base):
_id = 2
_entities = (_n,_n)
class PointInPlane(Base):
_id = 3
_entities = (_p,_w)
class PointOnLine(Base):
_id = 4
_entities = (_p,_l)
_workplane = True
class PointsDistance(Base):
_id = 5
_entities = (_p,_p)
_workplane = True
_props = [_PropertyDistance]
class PointsProjectDistance(Base):
_id = 6
_entities = (_p,_p,_l)
_props = [_PropertyDistance]
class PointPlaneDistance(Base):
_id = 7
_entities = (_p,_w)
_props = [_PropertyDistance]
class PointLineDistance(Base):
_id = 8
_entities = (_p,_l)
_workplane = True
_props = [_PropertyDistance]
class EqualLength(Base):
_id = 9
_entities = (_l,_l)
_workplane = True
class LengthRatio(Base):
_id = 10
_entities = (_l,_l)
_workplane = True
_props = [_PropertyRatio]
class LengthDifference(Base):
_id = 11
_entities = (_l,_l)
_workplane = True
_props = [_PropertyDifference]
class EqualLengthPointLineDistance(Base):
_id = 12
_entities = (_p,_l,_l)
_workplane = True
class EqualPointLineDistance(Base):
_id = 13
_entities = (_p,_l,_p,_l)
_workplane = True
class EqualAngle(Base):
_id = 14
_entities = (_l,_l,_l,_l)
_workplane = True
_props = [_PropertySupplement]
class EqualLineArcLength(Base):
_id = 15
_entities = (_l,_a)
_workplane = True
class Symmetric(Base):
_id = 16
_entities = (_p,_p,_w)
_workplane = True
class SymmetricHorizontal(Base):
_id = 17
_entities = (_p,_p,_w)
class SymmetricVertical(Base):
_id = 18
_entities = (_p,_p,_w)
class SymmetricLine(Base):
_id = 19
_entities = (_p,_p,_l,_w)
class MidPoint(Base):
_id = 20
_entities = (_p,_p,_l)
_workplane = True
class PointsHorizontal(Base):
_id = 21
_entities = (_p,_p)
_workplane = True
class PointsVertical(Base):
_id = 22
_entities = (_p,_p)
_workplane = True
class LineHorizontal(Base):
_id = 23
_entities = [_l]
_workplane = True
class LineVertical(Base):
_id = 24
_entities = [_l]
_workplane = True
class Diameter(Base):
_id = 25
_entities = [_c]
_prop = [_PropertyDiameter]
class PointOnCircle(Base):
_id = 26
_entities = [_p,_c]
class Angle(Base):
_id = 27
_entities = (_l,_l)
_workplane = True
_props = [_PropertyAngle,_PropertySupplement]
class Perpendicular(Base):
_id = 28
_entities = (_l,_l)
_workplane = True
class Parallel(Base):
_id = 29
_entities = (_l,_l)
_workplane = True
class ArcLineTangent(Base):
_id = 30
_entities = (_c,_l)
_props = [_PropertyAtEnd]
# class CubicLineTangent(Base):
# _id = 31
#
#
# class CurvesTangent(Base):
# _id = 32
class EqualRadius(Base):
_id = 33
_entities = (_c,_c)
_props = [_PropertyRadius]
class WhereDragged(Base):
_id = 34
_entities = [_p]
_workplane = True
TypeEnum = namedtuple('AsmConstraintEnum',
(c.getName() for c in Types))(*range(len(Types)))
def attach(obj,checkType=True):
props = None
if checkType:
props = obj.PropertiesList
if not '_Type' in props:
raise RuntimeError('Object "{}" has no _Type property'.format(
objName(obj)))
if 'Type' in props:
raise RuntimeError('Object {} already as property "Type"'.format(
objName(obj)))
# The 'Type' property here is to let user select the type in property
# editor. It is marked as 'transient' to avoid having to save the
# enumeration value for each object.
obj.addProperty("App::PropertyEnumeration","Type","Constraint",'',2)
obj.Type = TypeEnum._fields
idx = 0
try:
idx = TypeMap[obj._Type]._idx
except AttributeError:
logger.warn('{} has unknown constraint type {}'.format(
objName(obj),obj._Type))
obj.Type = idx
constraintType = TypeNameMap[obj.Type]
cstr = getattr(obj.Proxy,'_cstr',None)
if type(cstr) is not constraintType:
if cstr:
cstr.detach(obj)
if not props:
props = obj.PropertiesList
obj.Proxy._cstr = constraintType(obj,props)
def onChanged(obj,prop):
if prop == 'Type':
attach(obj,False)
return
elif prop == '_Type':
obj.Type = TypeMap[obj._Type]._idx
return
cstr = getattr(obj.Proxy,'_cstr',None)
if cstr:
cstr.onChanged(obj,prop)
def check(tp,group):
TypeMap[tp].check(group)
def prepare(cstr,solver):
cstr.Proxy._cstr.prepare(cstr,solver)

153
solver.py Normal file
View File

@ -0,0 +1,153 @@
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
class AsmSolver(object):
def __init__(self,assembly,reportFailed):
cstrs = assembly.Proxy.getConstraints()
if not cstrs:
logger.debug('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)))
return
self.system = slvs.System()
self._fixedGroup = 2
self.system.GroupHandle = 3 # the import group
self.group = 1 # the solving group
self._partMap = {}
self._cstrMap = {}
self._entityMap = {}
self._fixedPart = parts[0]
for cstr in cstrs:
logger.debug('preparing {}, type {}'.format(
objName(cstr),cstr.Type))
self._cstrMap[constraint.prepare(cstr,self)] = cstr
logger.debug('solving {}'.format(objName(assembly)))
ret = self.system.solve(group=self.group,reportFailed=reportFailed)
if ret:
if reportFailed:
msg = 'List of failed constraint:'
for f in self.system.Failed:
msg += '\n' + objName(self._cstrMap[f])
logger.error(msg)
raise RuntimeError('Failed to solve the constraints ({}) '
'in {}'.format(ret,objName(assembly)))
logger.debug('done sloving, dof {}'.format(self.system.Dof))
undoDocs = set()
for part,partInfo in self._partMap.items():
if part == self._fixedPart:
continue
params = [ self.system.getParam(h).val for h in partInfo.Params ]
p = params[:3]
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))
else:
logger.debug('moving {} {} {} {}'.format(
partInfo.PartName,partInfo.Params,params,pla))
asm.AsmElementLink.setPlacement(part,pla,undoDocs)
for doc in undoDocs:
doc.commitTransaction()
def getPartInfo(self,info):
partInfo = self._partMap.get(info.Part,None)
if partInfo:
return partInfo
# info.Object below is supposed to be the actual part geometry, while
# info.Part may be a link to that object. We use info.Object as a key so
# that multiple info.Part can share the same entity map.
#
# TODO: It is actually more complicated than that. Becuase info.Object
# itself may be a link, and followed by a chain of multiple links. It's
# complicated because each link can either have a linked placement or
# not, depends on its LinkTransform property, meaning that their
# Placement may be chained or independent. Ideally, we should explode
# the link chain, and recreate the transformation dependency using slvs
# transfomation entity. We'll leave that for another day, maybe...
#
# The down side for now is that we may have redundant entities, and
# worse, the solver may not be able to get correct result if there are
# placement dependent links among parts. So, one should avoid using
# LinkTransform in most cases.
#
entityMap = self._entityMap.get(info.Object,None)
if not entityMap:
entityMap = {}
self._entityMap[info.Object] = entityMap
if info.Part == self._fixedPart:
g = self._fixedGroup
else:
g = self.group
q = info.Placement.Rotation.Q
vals = list(info.Placement.Base) + [q[3],q[0],q[1],q[2]]
params = [self.system.addParamV(v,g) for v in vals]
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)
logger.debug('{} {}, {}, {}, {}'.format(
info.PartName,info.Placement,h,params,vals))
partInfo = constraint.PartInfo(
info.PartName, info.Placement.copy(),params,h,entityMap)
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')
return
if recompute:
docs = set()
for o in objs:
docs.add(o.Document)
for d in docs:
logger.debug('recomputing {}'.format(d.Name))
d.recompute()
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):
logger.debug('adding assembly {}'.format(objName(obj)))
assemblies.append(obj)
if not assemblies:
logger.error('no assembly found')
return
for assembly in assemblies:
logger.debug('solving assembly {}'.format(objName(assembly)))
AsmSolver(assembly,reportFailed)
if recompute:
assembly.Document.recompute()

341
utils.py Normal file
View File

@ -0,0 +1,341 @@
'''
Collection of helper function to extract geometry properties from OCC elements
Most of the functions are borrowed directly from assembly2lib.py or lib3D.py in
assembly2
'''
import FreeCAD, FreeCADGui, Part
import numpy
import asm3.FCADLogger
logger = asm3.FCADLogger.FCADLogger('assembly3')
def objName(obj):
if obj.Label == obj.Name:
return obj.Name
return '{}({})'.format(obj.Name,obj.Label)
def isLine(param):
if hasattr(Part,"LineSegment"):
return isinstance(param,(Part.Line,Part.LineSegment))
else:
return isinstance(param,Part.Line)
def getElement(obj,tp):
if isinstance(obj,tuple):
obj = obj[0].getSubObject(obj[1])
if not isinstance(obj,Part.Shape):
return
if obj.isNull() or not tp:
return
if tp == 'Vertex':
vertexes = obj.Vertexes
if len(vertexes)==1 and not obj.Edges:
return vertexes[0]
elif tp == 'Edge':
edges = obj.Edges
if len(edges)==1 and not obj.Faces:
return edges[0]
elif tp == 'Face':
faces = obj.Faces
if len(faces)==1:
return faces[0]
def isPlane(obj):
face = getElement(obj,'Face')
if not face:
return False
elif str(face.Surface) == '<Plane object>':
return True
elif hasattr(face.Surface,'Radius'):
return False
elif str(face.Surface).startswith('<SurfaceOfRevolution'):
return False
else:
_plane_norm,_plane_pos,error = fit_plane_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
return error_normalized < 10**-6
def isCylindricalPlane(obj):
face = getElement(obj,'Face')
if not face:
return False
elif hasattr(face.Surface,'Radius'):
return True
elif str(face.Surface).startswith('<SurfaceOfRevolution'):
return True
elif str(face.Surface) == '<Plane object>':
return False
else:
_axis,_center,error=fit_rotation_axis_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
return error_normalized < 10**-6
def isAxisOfPlane(obj):
face = getElement(obj,'Face')
if not face:
return False
if str(face.Surface) == '<Plane object>':
return True
else:
_axis,_center,error=fit_rotation_axis_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
return error_normalized < 10**-6
def isCircularEdge(obj):
edge = getElement(obj,'Edge')
if not edge:
return False
elif not hasattr(edge, 'Curve'): #issue 39
return False
if hasattr( edge.Curve, 'Radius' ):
return True
elif isLine(edge.Curve):
return False
else:
BSpline = edge.Curve.toBSpline()
try:
arcs = BSpline.toBiArcs(10**-6)
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 )
return max(sigma) < 10**-6
return False
def isLinearEdge(obj):
edge = getElement(obj,'Edge')
if not edge:
return False
elif not hasattr(edge, 'Curve'): #issue 39
return False
if isLine(edge.Curve):
return True
elif hasattr( edge.Curve, 'Radius' ):
return False
else:
BSpline = edge.Curve.toBSpline()
try:
arcs = BSpline.toBiArcs(10**-6)
except Exception: #FreeCAD exception thrown ()
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
return False
def isVertex(obj):
return getElement(obj,'Vertex') is not None
def hasCenter(obj):
return isVertex(obj) or isCircularEdge(obj) or \
isAxisOfPlane(obj) or isSphericalSurface(obj)
def isSphericalSurface(obj):
face = getElement(obj,'Face')
if not face:
return False
return str( face.Surface ).startswith('Sphere ')
def getElementPos(obj):
pos = None
vertex = getElement(obj,'Vertex')
if vertex:
return vertex.Point
face = getElement(obj,'Face')
if face:
surface = face.Surface
if str(surface) == '<Plane object>':
# pos = face.BoundBox.Center
pos = surface.Position
elif all( hasattr(surface,a) for a in ['Axis','Center','Radius'] ):
pos = surface.Center
elif str(surface).startswith('<SurfaceOfRevolution'):
pos = face.Edges1.Curve.Center
else: #numerically approximating surface
_plane_norm, plane_pos, error = \
fit_plane_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
if error_normalized < 10**-6: #then good plane fit
pos = plane_pos
_axis, center, error = \
fit_rotation_axis_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
if error_normalized < 10**-6: #then good rotation_axis fix
pos = center
else:
edge = getElement(obj,'Edge')
if edge:
if isLine(edge.Curve):
pos = edge.Vertexes[-1].Point
elif hasattr( edge.Curve, 'Center'): #circular curve
pos = edge.Curve.Center
else:
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 )
if max(sigma) < 10**-6: #then circular curce
pos = centers[0]
elif all(isLine(a) for a in arcs):
lines = arcs
D = numpy.array(
[L.tangent(0)[0] for L in lines]) #D(irections)
if numpy.std( D, axis=0 ).max() < 10**-9: #then linear curve
return lines[0].value(0)
return pos
def getElementAxis(obj):
axis = None
face = getElement(obj,'Face')
if face:
surface = face.Surface
if hasattr(surface,'Axis'):
axis = surface.Axis
elif str(surface).startswith('<SurfaceOfRevolution'):
axis = face.Edges[0].Curve.Axis
else: #numerically approximating surface
plane_norm, _plane_pos, error = \
fit_plane_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
if error_normalized < 10**-6: #then good plane fit
axis = plane_norm
axis_fitted, _center, error = \
fit_rotation_axis_to_surface1(face.Surface)
error_normalized = error / face.BoundBox.DiagonalLength
if error_normalized < 10**-6: #then good rotation_axis fix
axis = axis_fitted
else:
edge = getElement(obj,'Edge')
if edge:
if isLine(edge.Curve):
axis = edge.Curve.tangent(0)[0]
elif hasattr( edge.Curve, 'Axis'): #circular curve
axis = edge.Curve.Axis
else:
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 )
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(
[L.tangent(0)[0] for L in lines]) #D(irections)
if numpy.std( D, axis=0 ).max() < 10**-9: #then linear curve
return D[0]
return axis
def axisToNormal(v):
return FreeCAD.Rotation(FreeCAD.Vector(0,0,1),v).Q
def getElementNormal(obj):
return axisToNormal(getElementAxis(obj))
def getElementCircular(obj):
'return radius if it is closed, or a list of two endpoints'
edge = getElement(obj,'Edge')
if not edge:
return
elif not hasattr(edge, 'Curve'): #issue 39
return
c = edge.Curve
if hasattr( c, 'Radius' ):
if edge.Closed:
return c.Radius
elif isLine(edge.Curve):
return
else:
BSpline = edge.Curve.toBSpline()
try:
arc = BSpline.toBiArcs(10**-6)[0]
except Exception: #FreeCAD exception thrown ()
return
if edge.Closed:
return arc[0].Radius
return [v.Point for v in edge.Vertexes]
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) ], [] )
# 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 ]
# 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 ])
return plane_norm, plane_pos, error
def fit_rotation_axis_to_surface1( surface, n_u=3, n_v=3 ):
'''
should work for cylinders and pssibly cones (depending on the u,v mapping)
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) ], [] )
# 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 ]
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:
continue #ignore parrallel case
p1_x, p1_y, p1_z = P[i]
u1_x, u1_y, u1_z = N[i]
p2_x, p2_y, p2_z = P[j]
u2_x, u2_y, u2_z = N[j]
t1_t1_coef = u1_x**2 + u1_y**2 + u1_z**2 #should equal 1
# collect( expand(d_sqrd), [t1*t2] )
t1_t2_coef = -2*u1_x*u2_x - 2*u1_y*u2_y - 2*u1_z*u2_z
t2_t2_coef = u2_x**2 + u2_y**2 + u2_z**2 #should equal 1 too
t1_coef = 2*p1_x*u1_x + 2*p1_y*u1_y + 2*p1_z*u1_z - \
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 ],
[ t1_t2_coef, 2*t2_t2_coef ] ])
b = numpy.array([ t1_coef, t2_coef])
try:
t1, t2 = numpy.linalg.solve(A,-b)
except numpy.linalg.LinAlgError:
continue
pos_t1 = P[i] + numpy.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
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,
# sorted in descending order.
_U,s,V = numpy.linalg.svd(A)
axis_pos = centroid
axis_dir = V[0]
error = s[1] #dont know if this will work
return axis_dir, axis_pos, error
_tol = 10e-7
def isSamePlacement(pla1,pla2):
return pla1.Base.distanceToPoint(pla2.Base) < _tol and \
numpy.norm(numpy.array(pla1.Rotation.Q) - \
numpy.array(pla2.Rotation.Q)) < _tol