Basic function working
Tested PointsCoincident constraint
This commit is contained in:
parent
402d7a1ef4
commit
b5a91ec889
66
FCADLogger.py
Normal file
66
FCADLogger.py
Normal 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
26
__init__.py
Normal 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
982
assembly.py
Normal 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
521
constraint.py
Normal 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
153
solver.py
Normal 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
341
utils.py
Normal 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
|
Loading…
Reference in New Issue
Block a user