4860 lines
172 KiB
Python
4860 lines
172 KiB
Python
import os, traceback
|
|
from collections import namedtuple,defaultdict
|
|
import FreeCAD, FreeCADGui, Part
|
|
from PySide import QtCore, QtGui
|
|
from . import utils, gui
|
|
from .utils import mainlogger as logger, objName
|
|
from .constraint import Constraint, cstrName
|
|
from .system import System
|
|
|
|
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
|
|
|
|
def hasProperty(obj,prop):
|
|
try:
|
|
obj.getPropertyByName(prop,1)
|
|
return True
|
|
except AttributeError:
|
|
return False
|
|
except Exception:
|
|
# work around for older FC where getPropertyByName() only accepts one
|
|
# argument
|
|
try:
|
|
obj.getPropertyByName(prop)
|
|
except Exception:
|
|
return False
|
|
if obj.isDerivedFrom('App::DocumentObject'):
|
|
linked = obj.getLinkedObject(True)
|
|
elif obj.isDerivedFrom('Gui::ViewProviderDocumentObject'):
|
|
linked = obj.Object.getLinkedObject(True).ViewObject
|
|
else:
|
|
return True
|
|
return linked == obj or not hasattr(linked,prop)
|
|
|
|
def getLinkProperty(obj,name,default=None,writable=False):
|
|
try:
|
|
# obj = obj.getLinkedObject(True)
|
|
if not writable:
|
|
return obj.getLinkExtProperty(name)
|
|
name = obj.getLinkExtPropertyName(name)
|
|
if 'Immutable' in obj.getPropertyStatus(name):
|
|
return default
|
|
return getattr(obj,name)
|
|
except Exception:
|
|
return default
|
|
|
|
def setLinkProperty(obj,name,val):
|
|
# obj = obj.getLinkedObject(True)
|
|
setattr(obj,obj.getLinkExtPropertyName(name),val)
|
|
|
|
def flattenSubname(obj,subname):
|
|
'''
|
|
Falttern any AsmPlainGroups inside subname path. Only the first encountered
|
|
assembly along the subname path is considered
|
|
'''
|
|
|
|
func = getattr(obj,'flattenSubname',None)
|
|
if not func:
|
|
return subname
|
|
return func(subname)
|
|
|
|
def flattenLastSubname(obj,subname,last=None):
|
|
'''
|
|
Falttern any AsmPlainGroups inside subname path. Only the last encountered
|
|
assembly along the subname path is considered
|
|
'''
|
|
if not last:
|
|
last = Assembly.find(obj,subname,
|
|
relativeToChild=True,recursive=True)[-1]
|
|
return subname[:-len(last.Subname)] \
|
|
+ flattenSubname(last.Object,last.Subname)
|
|
|
|
def expandSubname(obj,subname):
|
|
func = getattr(obj,'expandSubname',None)
|
|
if not func:
|
|
return subname
|
|
return func(subname)
|
|
|
|
def flattenGroup(obj):
|
|
group = getattr(obj,'LinkedChildren',None)
|
|
if group is None:
|
|
return obj.Group
|
|
return group
|
|
|
|
def editGroup(obj,children,notouch=None):
|
|
change = None
|
|
if 'Immutable' in obj.getPropertyStatus('Group'):
|
|
change = '-Immutable'
|
|
revert = 'Immutable'
|
|
|
|
parent = getattr(obj,'_Parent',None)
|
|
if parent and 'Touched' in parent.State:
|
|
parent = None
|
|
|
|
if not hasProperty(obj,'NoTouch'):
|
|
notouch = False
|
|
elif notouch is None:
|
|
if (isTypeOf(parent,AsmConstraintGroup) or \
|
|
isTypeOf(obj,AsmConstraintGroup)):
|
|
# the order inside constraint group actually matters, so do not
|
|
# engage no touch
|
|
parent = None
|
|
else:
|
|
notouch = not obj.NoTouch
|
|
|
|
if notouch:
|
|
obj.NoTouch = True
|
|
block = gui.AsmCmdManager.AutoRecompute
|
|
if block:
|
|
gui.AsmCmdManager.AutoRecompute = False
|
|
try:
|
|
if change:
|
|
obj.setPropertyStatus('Group',change)
|
|
obj.Group = children
|
|
finally:
|
|
if change:
|
|
obj.setPropertyStatus('Group',revert)
|
|
if block:
|
|
gui.AsmCmdManager.AutoRecompute = True
|
|
if notouch:
|
|
obj.NoTouch = False
|
|
if parent:
|
|
parent.purgeTouched()
|
|
|
|
def setupSortMenu(menu,func,func2):
|
|
action = QtGui.QAction(QtGui.QIcon(),"Sort element A~Z",menu)
|
|
QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),func)
|
|
menu.addAction(action)
|
|
action = QtGui.QAction(QtGui.QIcon(),"Sort element Z~A",menu)
|
|
QtCore.QObject.connect(
|
|
action,QtCore.SIGNAL("triggered()"),func2)
|
|
menu.addAction(action)
|
|
|
|
def sortChildren(obj,reverse):
|
|
group = [ (o,o.Label) for o in obj.Group ]
|
|
group = sorted(group,reverse=reverse,key=lambda x:x[1])
|
|
touched = 'Touched' in obj.State
|
|
FreeCAD.setActiveTransaction('Sort children')
|
|
try:
|
|
editGroup(obj, [o[0] for o in group])
|
|
FreeCAD.closeActiveTransaction()
|
|
except Exception:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
if not touched:
|
|
obj.purgeTouched()
|
|
|
|
def resolveAssembly(obj):
|
|
'''Try various ways to obtain an assembly from the input object
|
|
|
|
obj can be a link, a proxy, a child group of an assembly, or simply an
|
|
assembly
|
|
'''
|
|
func = getattr(obj,'getLinkedObject',None)
|
|
if func:
|
|
obj = func(True)
|
|
proxy = getattr(obj,'Proxy',None)
|
|
if proxy:
|
|
obj = proxy
|
|
if isinstance(obj,Assembly):
|
|
return obj
|
|
func = getattr(obj,'getAssembly',None)
|
|
if func:
|
|
return func()
|
|
raise TypeError('cannot resolve assembly from {}'.format(obj))
|
|
|
|
|
|
# For faking selection obtained from Gui.getSelectionEx()
|
|
Selection = namedtuple('AsmSelection',('Object','SubElementNames'))
|
|
|
|
_IgnoredProperties = set(['VisibilityList','Visibility',
|
|
'Label','_LinkTouched'])
|
|
|
|
class AsmBase(object):
|
|
def __init__(self):
|
|
self.Object = 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.Object = obj
|
|
return
|
|
|
|
def getViewProviderName(self,_obj):
|
|
return 'Gui::ViewProviderLinkPython'
|
|
|
|
def onDocumentRestored(self, obj):
|
|
self.linkSetup(obj)
|
|
|
|
|
|
class ViewProviderAsmBase(object):
|
|
def __init__(self,vobj):
|
|
vobj.Visibility = False
|
|
vobj.Proxy = self
|
|
self.attach(vobj)
|
|
|
|
def replaceObject(self,_new,_old):
|
|
return False
|
|
|
|
def canAddToSceneGraph(self):
|
|
return False
|
|
|
|
def attach(self,vobj):
|
|
if hasattr(self,'ViewObject'):
|
|
return
|
|
self.ViewObject = vobj
|
|
vobj.signalChangeIcon()
|
|
vobj.setPropertyStatus('Visibility','Hidden')
|
|
|
|
def __getstate__(self):
|
|
return None
|
|
|
|
def __setstate__(self, _state):
|
|
return None
|
|
|
|
_iconName = None
|
|
|
|
@classmethod
|
|
def getIcon(cls):
|
|
if cls._iconName:
|
|
return utils.getIcon(cls)
|
|
|
|
def canDropObjects(self):
|
|
return True
|
|
|
|
def canDragObjects(self):
|
|
return False
|
|
|
|
def canDragAndDropObject(self,_obj):
|
|
return False
|
|
|
|
|
|
class ViewProviderAsmOnTop(ViewProviderAsmBase):
|
|
def __init__(self,vobj):
|
|
vobj.OnTopWhenSelected = 2
|
|
super(ViewProviderAsmOnTop,self).__init__(vobj)
|
|
|
|
|
|
class AsmGroup(AsmBase):
|
|
def linkSetup(self,obj):
|
|
super(AsmGroup,self).linkSetup(obj)
|
|
obj.configLinkProperty(
|
|
'VisibilityList',LinkMode='GroupMode',ElementList='Group')
|
|
self.groupSetup()
|
|
|
|
def groupSetup(self):
|
|
self.Object.setPropertyStatus('GroupMode','-Immutable')
|
|
self.Object.GroupMode = 1 # auto delete children
|
|
self.Object.setPropertyStatus('GroupMode',
|
|
('Hidden','Immutable','Transient'))
|
|
self.Object.setPropertyStatus('Group',('Hidden','Immutable'))
|
|
# 'PartialTrigger' is just for silencing warning when partial load
|
|
self.Object.setPropertyStatus('VisibilityList',
|
|
('Output','PartialTrigger','NoModify'))
|
|
|
|
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 getattr(self.ViewObject.Object, 'Group', [])
|
|
|
|
def doubleClicked(self, _vobj):
|
|
return False
|
|
|
|
def canDropObject(self,_child):
|
|
return False
|
|
|
|
|
|
class ViewProviderAsmGroupOnTop(ViewProviderAsmGroup):
|
|
def __init__(self,vobj):
|
|
vobj.OnTopWhenSelected = 2
|
|
super(ViewProviderAsmGroupOnTop,self).__init__(vobj)
|
|
|
|
|
|
class AsmPartGroup(AsmGroup):
|
|
def __init__(self,parent):
|
|
self.parent = getProxy(parent,Assembly)
|
|
self.derivedParts = None
|
|
super(AsmPartGroup,self).__init__()
|
|
|
|
def getSubObjects(self,obj,_reason):
|
|
# Deletion order problem may cause exception here. Just silence it
|
|
try:
|
|
if not getattr(obj.Document,'Partial',False) \
|
|
or not self.getAssembly().Object.Freeze:
|
|
return [ '{}.'.format(o.Name) for o in flattenGroup(obj) ]
|
|
except Exception:
|
|
pass
|
|
|
|
def linkSetup(self,obj):
|
|
super(AsmPartGroup,self).linkSetup(obj)
|
|
if not hasProperty(obj,'DerivedFrom'):
|
|
obj.addProperty('App::PropertyLink','DerivedFrom','Base','')
|
|
try:
|
|
obj.setPropertyStatus('Shape','-Output')
|
|
except Exception:
|
|
pass
|
|
self.derivedParts = None
|
|
|
|
def checkDerivedParts(self):
|
|
if self.getAssembly().Object.Freeze:
|
|
return
|
|
|
|
obj = self.Object
|
|
if not isTypeOf(obj.DerivedFrom,Assembly,True):
|
|
self.derivedParts = None
|
|
return
|
|
|
|
parts = set(obj.LinkedObject)
|
|
derived = obj.DerivedFrom.getLinkedObject(True).Proxy.getPartGroup()
|
|
self.derivedParts = derived.LinkedObject
|
|
newParts = obj.Group
|
|
vis = list(obj.VisibilityList)
|
|
touched = False
|
|
for o in self.derivedParts:
|
|
if o in parts:
|
|
continue
|
|
touched = True
|
|
newParts.append(o)
|
|
vis.append(True if derived.isElementVisible(o.Name) else False)
|
|
if touched:
|
|
obj.Group = newParts
|
|
obj.setPropertyStatus('VisibilityList','-Immutable')
|
|
obj.VisibilityList = vis
|
|
obj.setPropertyStatus('VisibilityList','Immutable')
|
|
|
|
def getAssembly(self):
|
|
return self.parent
|
|
|
|
def groupSetup(self):
|
|
pass
|
|
|
|
def canLoadPartial(self,_obj):
|
|
return 1 if self.getAssembly().frozen else 0
|
|
|
|
def onChanged(self,obj,prop):
|
|
if obj.Removing or FreeCAD.isRestoring() :
|
|
return
|
|
if obj.Document and getattr(obj.Document,'Transacting',False):
|
|
return
|
|
if prop == 'DerivedFrom':
|
|
self.checkDerivedParts()
|
|
elif prop in ('Group','_ChildCache'):
|
|
parent = getattr(self,'parent',None)
|
|
if parent and not self.parent.Object.Freeze:
|
|
relationGroup = parent.getRelationGroup()
|
|
if relationGroup:
|
|
relationGroup.Proxy.getRelations(True)
|
|
|
|
@staticmethod
|
|
def make(parent,name='Parts'):
|
|
obj = parent.Document.addObject("Part::FeaturePython",name,
|
|
AsmPartGroup(parent),None,True)
|
|
obj.setPropertyStatus('Placement',('Output','Hidden'))
|
|
ViewProviderAsmPartGroup(obj.ViewObject)
|
|
obj.purgeTouched()
|
|
return obj
|
|
|
|
|
|
class ViewProviderAsmPartGroup(ViewProviderAsmGroup):
|
|
_iconName = 'Assembly_Assembly_Part_Tree.svg'
|
|
|
|
def replaceObject(self,new,old):
|
|
return self.Object.replaceObject(new,old)
|
|
|
|
def canDropObjectEx(self,obj,_owner,_subname,_elements):
|
|
return isTypeOf(obj,Assembly, True) or not isTypeOf(obj,AsmBase)
|
|
|
|
def dropObjectEx(self,vobj,obj,_owner,_subname,_elements):
|
|
me = vobj.Object
|
|
if AsmPlainGroup.tryMove(obj,me):
|
|
return obj.Name+'.'
|
|
me.setLink({-1:obj})
|
|
return me.Group[-1].Name + '.'
|
|
|
|
def _drop(self,obj,owner,subname,elements):
|
|
me = self.ViewObject.Object
|
|
group = me.Group
|
|
self.ViewObject.dropObject(obj,owner,subname,elements)
|
|
return [ o for o in me.Group if o not in group ]
|
|
|
|
def canDragObject(self,_obj):
|
|
return True
|
|
|
|
def canDragObjects(self):
|
|
return True
|
|
|
|
def canDragAndDropObject(self,obj):
|
|
return not AsmPlainGroup.contains(self.ViewObject.Object,obj)
|
|
|
|
def onDelete(self,_vobj,_subs):
|
|
return False
|
|
|
|
def canDelete(self,_obj):
|
|
return True
|
|
|
|
def showParts(self):
|
|
vobj = self.ViewObject
|
|
obj = vobj.Object
|
|
if not obj.isDerivedFrom('Part::FeaturePython'):
|
|
return
|
|
assembly = obj.Proxy.getAssembly().Object
|
|
if not assembly.ViewObject.ShowParts and \
|
|
(assembly.Freeze or (assembly.BuildShape!=BuildShapeNone and \
|
|
assembly.BuildShape!=BuildShapeCompound)):
|
|
mode = 1
|
|
else:
|
|
mode = 0
|
|
if not vobj.ChildViewProvider:
|
|
if not mode:
|
|
return
|
|
vobj.ChildViewProvider = 'PartGui::ViewProviderPartExt'
|
|
cvp = vobj.ChildViewProvider
|
|
try:
|
|
if not cvp.MapTransparency:
|
|
cvp.MapTransparency = True
|
|
if not cvp.MapFaceColor:
|
|
cvp.MapFaceColor = True
|
|
cvp.ForceMapColors = True
|
|
except Exception:
|
|
# exception here is normal for FC without topo naming
|
|
pass
|
|
vobj.DefaultMode = mode
|
|
|
|
def replaceObject(self,oldObj,newObj):
|
|
res = self.ViewObject.replaceObject(oldObj,newObj)
|
|
if res<=0:
|
|
return res
|
|
for obj in oldObj.InList:
|
|
if isTypeOf(obj,AsmElement):
|
|
link = obj.LinkedObject
|
|
if isinstance(link,tuple):
|
|
obj.setLink(newObj,link[1])
|
|
else:
|
|
obj.setLink(newObj)
|
|
return 1
|
|
|
|
|
|
class AsmVersion(object):
|
|
def __init__(self,v=None):
|
|
self.value = 0
|
|
self.childVersion = v
|
|
self._childVersion = v
|
|
self.updated = False
|
|
|
|
def update(self,v):
|
|
self.updated = False
|
|
if self.childVersion!=v:
|
|
self._childVersion = v
|
|
self.updated = True
|
|
return True
|
|
return not gui.AsmCmdManager.SmartRecompute
|
|
|
|
def commit(self):
|
|
if self.updated:
|
|
self.childVersion = self._childVersion
|
|
self.value += 1
|
|
self.updated = False
|
|
|
|
|
|
class AsmElement(AsmBase):
|
|
def __init__(self,parent):
|
|
self.version = None
|
|
self._initializing = True
|
|
self.parent = getProxy(parent,AsmElementGroup)
|
|
super(AsmElement,self).__init__()
|
|
|
|
# NOTE: Not bypassing default getLinkedObject() may affect PartFeature shape
|
|
# caching.
|
|
#
|
|
# def getLinkedObject(self,*_args):
|
|
# pass
|
|
|
|
def linkSetup(self,obj):
|
|
super(AsmElement,self).linkSetup(obj)
|
|
if not hasProperty(obj,'Offset'):
|
|
obj.addProperty("App::PropertyPlacement","Offset"," Link",'')
|
|
if not hasProperty(obj,'Placement'):
|
|
obj.addProperty("App::PropertyPlacement","Placement"," Link",'')
|
|
obj.setPropertyStatus('Placement','Hidden')
|
|
if not hasProperty(obj,'LinkTransform'):
|
|
obj.addProperty("App::PropertyBool","LinkTransform"," Link",'')
|
|
obj.LinkTransform = True
|
|
if not hasProperty(obj,'Detach'):
|
|
obj.addProperty('App::PropertyBool','Detach', ' Link','')
|
|
obj.setPropertyStatus('LinkTransform',['Immutable','Hidden'])
|
|
obj.setPropertyStatus('LinkedObject','ReadOnly')
|
|
obj.configLinkProperty('LinkedObject','Placement','LinkTransform')
|
|
|
|
parent = getattr(obj,'_Parent',None)
|
|
if parent:
|
|
self.parent = parent.Proxy
|
|
|
|
AsmElement.migrate(obj)
|
|
|
|
self.version = AsmVersion()
|
|
|
|
def canLoadPartial(self,_obj):
|
|
return 1 if self.getAssembly().frozen else 0
|
|
|
|
@staticmethod
|
|
def migrate(obj):
|
|
# To avoid over dependency, we no longer link to PartGroup, but to the
|
|
# child part object directly
|
|
link = obj.LinkedObject
|
|
if not isinstance(link,tuple):
|
|
return
|
|
if isTypeOf(link[0],AsmPartGroup):
|
|
logger.debug('migrate {}',objName(obj))
|
|
sub = link[1]
|
|
dot = sub.find('.')
|
|
sobj = link[0].getSubObject(sub[:dot+1],1)
|
|
touched = 'Touched' in obj.State
|
|
obj.setLink(sobj,sub[dot+1:])
|
|
if not touched:
|
|
obj.purgeTouched()
|
|
|
|
def attach(self,obj):
|
|
obj.addProperty("App::PropertyXLink","LinkedObject"," Link",'')
|
|
obj.addProperty("App::PropertyLinkHidden","_Parent"," Link",'')
|
|
obj._Parent = self.parent.Object
|
|
obj.setPropertyStatus('_Parent',('Hidden','Immutable'))
|
|
super(AsmElement,self).attach(obj)
|
|
|
|
def getViewProviderName(self,_obj):
|
|
return ''
|
|
|
|
def canLinkProperties(self,_obj):
|
|
return False
|
|
|
|
def allowDuplicateLabel(self,_obj):
|
|
return True
|
|
|
|
def onBeforeChangeLabel(self,obj,label):
|
|
parent = getattr(self,'parent',None)
|
|
if parent and not getattr(self,'_initializing',False):
|
|
return parent.onChildLabelChange(obj,label)
|
|
|
|
def autoName(self,obj):
|
|
oldLabel = getattr(obj,'OldLabel',None)
|
|
for link in FreeCAD.getLinksTo(obj,False):
|
|
if isTypeOf(link,AsmElementLink):
|
|
link.Label = obj.Label
|
|
elif isTypeOf(link,AsmElement):
|
|
if link.Label == link.Name:
|
|
if link.Label.startswith('_') and \
|
|
not obj.Label.startswith('_'):
|
|
link.Label = '_' + obj.Label
|
|
else:
|
|
link.Label = obj.Label
|
|
continue
|
|
|
|
if not oldLabel:
|
|
continue
|
|
|
|
if link.Label.startswith(oldLabel):
|
|
prefix = obj.Label
|
|
postfix = link.Label[len(oldLabel):]
|
|
elif link.Label.startswith('_'+oldLabel):
|
|
prefix = '_' + obj.Label
|
|
postfix = link.Label[len(oldLabel)+1:]
|
|
else:
|
|
continue
|
|
try:
|
|
int(postfix)
|
|
# ignore all digits postfix
|
|
link.Label = prefix
|
|
except Exception:
|
|
link.Label = prefix + postfix
|
|
|
|
def onChanged(self,obj,prop):
|
|
parent = getattr(self,'parent',None)
|
|
if not parent or obj.Removing or FreeCAD.isRestoring():
|
|
return
|
|
if obj.Document and getattr(obj.Document,'Transacting',False):
|
|
if prop == 'Label':
|
|
parent.Object.cacheChildLabel()
|
|
return
|
|
if prop=='Offset':
|
|
self.updatePlacement()
|
|
return
|
|
elif prop == 'Label':
|
|
self.autoName(obj)
|
|
# have to call cacheChildLabel() later, because those label
|
|
# referenced links is only auto corrected after onChanged()
|
|
parent.Object.cacheChildLabel()
|
|
|
|
if prop not in _IgnoredProperties and \
|
|
not Constraint.isDisabled(parent.Object):
|
|
Assembly.autoSolve(obj,prop)
|
|
|
|
def isBroken(self):
|
|
linked,subname = self.Object.LinkedObject
|
|
obj = linked.getSubObject(subname,1)
|
|
if isTypeOf(obj, AsmElement):
|
|
return obj.Proxy.isBroken()
|
|
if not obj:
|
|
return False # broken beyond fix
|
|
|
|
if not utils.getElement(linked, subname):
|
|
return True
|
|
|
|
def fix(self):
|
|
linked,subname = self.Object.LinkedObject
|
|
obj = linked.getSubObject(subname,1)
|
|
if isTypeOf(obj, AsmElement):
|
|
obj.Proxy.fix()
|
|
return
|
|
if not obj:
|
|
raise RuntimeError('Broken link')
|
|
subs = Part.splitSubname(subname)
|
|
if not subs[1]:
|
|
raise RuntimeError('No mapped sub-element found')
|
|
|
|
shape = Part.getShape(linked,subs[0])
|
|
if utils.getElement(shape, subs[1]):
|
|
return
|
|
|
|
for mapped, element in Part.getRelatedElements(linked, subname):
|
|
logger.msg('{} reference change {} {} -> {}',
|
|
self.Object.FullName, linked.FullName,
|
|
subs[0], subs[2], element)
|
|
subname = Part.joinSubname(subs[0],mapped,element)
|
|
self.Object.setLink(linked,subname)
|
|
return
|
|
|
|
if not hasattr(shape, 'searchSubShape'):
|
|
raise RuntimeError('Failed to fix element')
|
|
|
|
try:
|
|
myShape = self.Object.Shape.SubShapes[0]
|
|
except Exception:
|
|
raise RuntimeError('No element shape saved')
|
|
|
|
if myShape.countElement('Face')==1:
|
|
myShape = myShape.Face1
|
|
elif myShape.countElement('Edge')==1:
|
|
myShape = myShape.Edge1
|
|
elif myShape.countElement('Vertex')==1:
|
|
myShape = myShape.Vertex1
|
|
else:
|
|
raise RuntimeError('Unsupported element shape')
|
|
|
|
try:
|
|
element,_ = shape.searchSubShape(myShape,True)[0]
|
|
logger.msg('{} reference change {}.{} {} -> {}',
|
|
self.Object.FullName, linked.FullName,
|
|
subs[0], subs[2], element)
|
|
subname = Part.joinSubname(subs[0],'',element)
|
|
self.Object.setLink(linked,subname)
|
|
return
|
|
except Exception:
|
|
raise RuntimeError('Matching element shape not found')
|
|
|
|
def execute(self,obj):
|
|
if not obj.isDerivedFrom('Part::FeaturePython'):
|
|
self.version.value += 1
|
|
return False
|
|
|
|
if self.getAssembly().Object.Freeze:
|
|
logger.warn('Skip recomputing frozen element {}', objName(obj))
|
|
return True
|
|
|
|
if obj.Detach:
|
|
self.updatePlacement()
|
|
return True
|
|
|
|
info = None
|
|
try:
|
|
info = self.getInfo(False)
|
|
except Exception as e:
|
|
logger.warn(str(e))
|
|
|
|
self.updatePlacement()
|
|
|
|
if not gui.AsmCmdManager.AutoFixElement:
|
|
raise
|
|
|
|
self.fix()
|
|
info = self.getInfo(False)
|
|
|
|
if not getattr(obj,'Radius',None):
|
|
shape = Part.Shape(info.Shape).copy()
|
|
else:
|
|
if isinstance(info.Part,tuple):
|
|
parentShape = Part.getShape(info.Part[2], info.Subname,
|
|
transform=info.Part[3], needSubElement=False)
|
|
else:
|
|
parentShape = Part.getShape(info.Part, info.Subname,
|
|
transform=False, needSubElement=False)
|
|
found = False
|
|
shapes = [info.Shape]
|
|
pla = info.Shape.Placement
|
|
for edge in parentShape.Edges:
|
|
if not info.Shape.isCoplanar(edge) or \
|
|
not utils.isSameValue(
|
|
utils.getElementCircular(edge,True),obj.Radius):
|
|
continue
|
|
edge = edge.copy()
|
|
if not found and utils.isSamePlacement(pla,edge.Placement):
|
|
found = True
|
|
# make sure the direct referenced edge is the first one
|
|
shapes[0] = edge
|
|
else:
|
|
shapes.append(edge)
|
|
shape = shapes
|
|
|
|
# Make a compound to contain shape's part-local-placement. A second
|
|
# level compound will be made inside updatePlacement() to contain the
|
|
# part's placement.
|
|
shape = Part.makeCompound(shape)
|
|
try:
|
|
shape.ElementMap = info.Shape.ElementMap
|
|
except Exception:
|
|
pass
|
|
self.updatePlacement(info.Placement,shape)
|
|
return True
|
|
|
|
def updatePlacement(self,pla=None,shape=None):
|
|
obj = self.Object
|
|
if not shape:
|
|
# If the shape is not given, we simply obtain the shape inside our
|
|
# own "Shape" property
|
|
shape = obj.Shape
|
|
if not shape or shape.isNull():
|
|
return
|
|
# De-compound to obtain the original shape in our coordinate system
|
|
shape = shape.SubShapes[0]
|
|
|
|
# Call getElementInfo() to obtain part's placement only. We don't
|
|
# need the shape here, in order to handle missing down-stream
|
|
# element
|
|
info = self.getInfo()
|
|
pla = info.Placement
|
|
|
|
if obj.Offset.isIdentity():
|
|
objPla = FreeCAD.Placement()
|
|
else:
|
|
if hasProperty(obj,'Radius'):
|
|
s = shape.SubShapes[0]
|
|
else:
|
|
s = shape
|
|
# obj.Offset is in the element shape's coordinate system, we need to
|
|
# transform it to the assembly coordinate system
|
|
mat = pla.multiply(utils.getElementPlacement(s)).toMatrix()
|
|
objPla = FreeCAD.Placement(mat*obj.Offset.toMatrix()*mat.inverse())
|
|
|
|
# Update the shape with its owner Part's current placement
|
|
shape.Placement = pla
|
|
|
|
# Make a compound to contain the part's placement. There may be
|
|
# additional placement for this element which is updated below
|
|
shape = Part.makeCompound(shape)
|
|
obj.Shape = shape
|
|
obj.Placement = objPla
|
|
|
|
# unfortunately, we can't easily check two shapes are the same
|
|
self.version.value += 1
|
|
|
|
def getAssembly(self):
|
|
return self.parent.parent
|
|
|
|
def getSubElement(self):
|
|
link = self.Object.LinkedObject
|
|
if isinstance(link,tuple):
|
|
return link[1].split('.')[-1]
|
|
return ''
|
|
|
|
def getSubName(self):
|
|
link = self.Object.LinkedObject
|
|
if not link:
|
|
raise RuntimeError('Invalid element "{}"'.format(
|
|
objName(self.Object)))
|
|
if not isinstance(link,tuple):
|
|
return link.Name + '.'
|
|
return link[0].Name + '.' + link[1]
|
|
|
|
def getElementSubname(self,recursive=False):
|
|
'''
|
|
Recursively resolve the geometry element link relative to the parent
|
|
assembly's part group
|
|
'''
|
|
|
|
subname = self.getSubName()
|
|
if not recursive:
|
|
return subname
|
|
|
|
link = self.Object.LinkedObject
|
|
if not isinstance(link,tuple):
|
|
raise RuntimeError('Broken element link')
|
|
obj = link[0].getSubObject(link[1],1)
|
|
if not obj:
|
|
if self.getAssembly().Object.Freeze:
|
|
raise RuntimeError('Unable to resolve element on frozen assembly %s'\
|
|
% objName(self.getAssembly().Object))
|
|
raise RuntimeError('Broken element link %s.%s'%(objName(link[0]), link[1]))
|
|
if not isTypeOf(obj,AsmElement):
|
|
# If not pointing to another element, then assume we are directly
|
|
# pointing to the geometry element, just return as it is, which is a
|
|
# subname relative to the parent assembly part group
|
|
return subname
|
|
|
|
childElement = obj.Proxy
|
|
|
|
# If pointing to another element in the child assembly, first pop two
|
|
# names in the subname reference, i.e. element label and element group
|
|
# name
|
|
idx = subname.rfind('.',0,subname.rfind('.',0,-1))
|
|
subname = subname[:idx+1]
|
|
|
|
# append the child assembly part group name, and recursively call into
|
|
# child element
|
|
return subname+'2.'+childElement.getElementSubname(True)
|
|
|
|
# Element: optional, if none, then a new element will be created if no
|
|
# pre-existing. Or else, it shall be the element to be amended
|
|
# Group: the immediate child object of an assembly (i.e. ConstraintGroup,
|
|
# ElementGroup, or PartGroup)
|
|
# Subname: the subname reference relative to 'Group'
|
|
Selection = namedtuple('AsmElementSelection',('Element','Group','Subname',
|
|
'SelObj', 'SelSubname'))
|
|
|
|
@staticmethod
|
|
def getSelections():
|
|
'Parse Gui.Selection for making one or more elements'
|
|
|
|
sels = FreeCADGui.Selection.getSelectionEx('',False)
|
|
if not sels:
|
|
raise RuntimeError('no selection')
|
|
if not sels[0].SubElementNames:
|
|
raise RuntimeError('no sub-object in selection')
|
|
if len(sels)>1:
|
|
raise RuntimeError('too many selection')
|
|
|
|
hierarchies = []
|
|
assembly = None
|
|
element = None
|
|
selObj = sels[0].Object
|
|
selSubname = None
|
|
for sub in sels[0].SubElementNames:
|
|
path = Assembly.findChildren(selObj,sub)
|
|
if not path:
|
|
raise RuntimeError('no assembly in selection {}.{}'.format(
|
|
objName(selObj),sub))
|
|
if not path[-1].Object or \
|
|
path[-1].Subname.index('.')+1==len(path[-1].Subname):
|
|
if assembly:
|
|
raise RuntimeError('invalid selection')
|
|
assembly = path[-1].Assembly
|
|
selSubname = sub[:-len(path[-1].Subname)]
|
|
continue
|
|
|
|
elif isTypeOf(path[-1].Object,AsmElementGroup) and \
|
|
(not element or len(element)>len(path)):
|
|
if element:
|
|
hierarchies.append(element)
|
|
element = path
|
|
continue
|
|
|
|
hierarchies.append(path)
|
|
|
|
if not hierarchies:
|
|
if not element:
|
|
raise RuntimeError('no element selection')
|
|
hierarchies.append(element)
|
|
element = None
|
|
|
|
if element:
|
|
if len(hierarchies)>1:
|
|
raise RuntimeError('too many selections')
|
|
element = element[-1].Assembly.getSubObject(element[-1].Subname,1)
|
|
if not isTypeOf(element,AsmElement):
|
|
element = None
|
|
|
|
if not assembly:
|
|
path = hierarchies[0]
|
|
assembly = path[0].Assembly
|
|
selSubname = sels[0].SubElementNames[0][:-len(path[0].Subname)]
|
|
for i,hierarchy in enumerate(hierarchies):
|
|
for path in hierarchy:
|
|
if path.Assembly == assembly:
|
|
sub = path.Subname[path.Subname.index('.')+1:]
|
|
hierarchies[i] = AsmElement.Selection(
|
|
Element=element,
|
|
Group=path.Object,
|
|
Subname=sub,
|
|
SelObj=selObj,
|
|
SelSubname=selSubname)
|
|
break
|
|
else:
|
|
raise RuntimeError('parent assembly mismatch')
|
|
return hierarchies
|
|
|
|
@classmethod
|
|
def create(cls,name,elements):
|
|
if elements.Proxy.getAssembly().Object.Freeze:
|
|
raise RuntimeError('Cannot create new element in frozen assembly')
|
|
element = elements.Document.addObject("Part::FeaturePython",
|
|
name,cls(elements),None,True)
|
|
ViewProviderAsmElement(element.ViewObject)
|
|
return element
|
|
|
|
@staticmethod
|
|
def make(selection=None,name='Element',undo=False,
|
|
radius=None,allowDuplicate=False):
|
|
'''Add/get/modify an element with the given selected object'''
|
|
if not selection:
|
|
sels = AsmElement.getSelections()
|
|
if len(sels)==1:
|
|
ret = [AsmElement.make(sels[0],name,undo,radius,allowDuplicate)]
|
|
else:
|
|
if undo:
|
|
FreeCAD.setActiveTransaction('Assembly create element')
|
|
try:
|
|
ret = []
|
|
for sel in sels:
|
|
ret.append(AsmElement.make(
|
|
sel,name,False,radius,allowDuplicate))
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction()
|
|
if not ret:
|
|
return
|
|
except Exception:
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
|
|
FreeCADGui.Selection.pushSelStack()
|
|
FreeCADGui.Selection.clearSelection()
|
|
for obj in ret:
|
|
if sels[0].SelSubname:
|
|
subname = sels[0].SelSubname
|
|
else:
|
|
subname = ''
|
|
subname += '1.{}.'.format(obj.Name)
|
|
FreeCADGui.Selection.addSelection(sels[0].SelObj,subname)
|
|
FreeCADGui.Selection.pushSelStack()
|
|
FreeCADGui.runCommand('Std_TreeSelection')
|
|
return ret
|
|
|
|
group = selection.Group
|
|
subname = flattenSubname(selection.Group,selection.Subname)
|
|
|
|
if isTypeOf(group,AsmElementGroup):
|
|
# if the selected object is an element of the owner assembly, simply
|
|
# return that element
|
|
element = group.getSubObject(subname,1)
|
|
if not isTypeOf(element,AsmElement):
|
|
raise RuntimeError('Invalid element reference {}.{}'.format(
|
|
group.Name,subname))
|
|
if not allowDuplicate:
|
|
return element
|
|
group = element.getAssembly().getPartGroup()
|
|
subname = element.getSubName()
|
|
|
|
elif isTypeOf(group,AsmConstraintGroup):
|
|
# if the selected object is an element link of a constraint of the
|
|
# current assembly, then try to import its linked element if it is
|
|
# not already imported
|
|
link = group.getSubObject(subname,1)
|
|
if not isTypeOf(link,AsmElementLink):
|
|
raise RuntimeError('Invalid element link {}.{}'.format(
|
|
group.Name,subname))
|
|
ref = link.LinkedObject
|
|
if not isinstance(ref,tuple):
|
|
if not isTypeOf(ref,AsmElement):
|
|
raise RuntimeError('broken element link {}.{}'.format(
|
|
group.Name,subname))
|
|
return ref
|
|
if ref[1][0]=='$':
|
|
# this means the element is in the current assembly already
|
|
element = link.getLinkedObject(False)
|
|
if not isTypeOf(element,AsmElement):
|
|
raise RuntimeError('broken element link {}.{}'.format(
|
|
group.Name,subname))
|
|
return element
|
|
|
|
subname = ref[1]
|
|
group = group.Proxy.getAssembly().getPartGroup()
|
|
|
|
elif isTypeOf(group,AsmPartGroup):
|
|
# If the selection come from the part group, first check for any
|
|
# intermediate child assembly
|
|
ret = Assembly.find(group,subname)
|
|
if not ret:
|
|
# If no child assembly in 'subname', simply assign the link as
|
|
# it is, after making sure it is referencing an sub-element
|
|
if not utils.isElement((group,subname)):
|
|
raise RuntimeError( 'Element must reference a geometry '
|
|
'element {}.{}'.format(objName(group),subname))
|
|
else:
|
|
# In case there are intermediate assembly inside subname, we'll
|
|
# recursively export the element in child assemblies first, and
|
|
# then import that element to the current assembly.
|
|
sel = AsmElement.Selection(SelObj=None,SelSubname=None,
|
|
Element=None, Group=ret.Object, Subname=ret.Subname)
|
|
element = AsmElement.make(sel,radius=radius)
|
|
radius=None
|
|
|
|
# now generate the subname reference
|
|
|
|
# This give us reference to child assembly's immediate child
|
|
# without trailing dot.
|
|
prefix = subname[:-len(ret.Subname)-1]
|
|
|
|
# Pop the immediate child name
|
|
prefix = prefix[:prefix.rfind('.')]
|
|
|
|
# Finally, generate the subname, by combining the prefix with
|
|
# the element group index (i.e. the 1 below) and the linked
|
|
# element label
|
|
subname = '{}.1.${}.'.format(prefix,element.Label)
|
|
|
|
else:
|
|
raise RuntimeError('Invalid selection {}.{}'.format(
|
|
objName(group),subname))
|
|
|
|
element = selection.Element
|
|
|
|
subname = flattenSubname(group,subname)
|
|
dot = subname.find('.')
|
|
sobj = group.getSubObject(subname[:dot+1],1)
|
|
if not sobj:
|
|
raise RuntimeError('invalid link {}.{}'.format(
|
|
objName(group),subname))
|
|
try:
|
|
if undo:
|
|
FreeCAD.setActiveTransaction('Assembly change element' \
|
|
if element else 'Assembly create element')
|
|
|
|
elements = group.Proxy.getAssembly().getElementGroup()
|
|
idx = -1
|
|
newElement = False
|
|
if not element:
|
|
if not allowDuplicate:
|
|
# try to search the element group for an existing element
|
|
for e in flattenGroup(elements):
|
|
if not e.Offset.isIdentity():
|
|
continue
|
|
sub = logger.catch('',e.Proxy.getSubName)
|
|
if sub!=subname:
|
|
continue
|
|
r = getattr(e,'Radius',None)
|
|
if (not radius and not r) or radius==r:
|
|
return e
|
|
newElement = True
|
|
element = AsmElement.create(name,elements)
|
|
if radius:
|
|
element.addProperty('App::PropertyFloat','Radius','','')
|
|
element.Radius = radius
|
|
elements.setLink({idx:element})
|
|
elements.setElementVisible(element.Name,False)
|
|
element.Proxy._initializing = False
|
|
elements.cacheChildLabel()
|
|
|
|
element.setLink(sobj,subname[dot+1:])
|
|
if newElement:
|
|
linked = element.LinkedObject
|
|
objPath = None
|
|
if isinstance(linked,tuple):
|
|
objPath = logger.catchDebug('', linked[0].getSubObjectList, linked[1])
|
|
if objPath and len(objPath)>2 \
|
|
and isTypeOf(objPath[2], AsmElement) \
|
|
and objPath[2].Label != objPath[2].Name \
|
|
and objPath[2].Label != '_' + objPath[2].Name:
|
|
assembly = objPath[2].Proxy.getAssembly().Object
|
|
label = objPath[2].Label
|
|
idx = label.rfind('@')
|
|
if idx > 0:
|
|
label = label[:idx]
|
|
element.Label = label + '@' + assembly.Label
|
|
else:
|
|
target = element.getLinkedObject(True)
|
|
if target and (target.isDerivedFrom('App::OriginFeature') \
|
|
or target.isDerivedFrom('Part::Datum')):
|
|
label = target.Label
|
|
parent = target.getParentGeoFeatureGroup()
|
|
if parent:
|
|
label += '@' + parent.Label
|
|
element.Label = label
|
|
|
|
element.recompute()
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction()
|
|
except Exception:
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
return element
|
|
|
|
def getSubObject(self,obj,subname,retType,mat,transform,depth):
|
|
if subname in ('X', 'Y', 'Z'):
|
|
subname = ''
|
|
return obj.getSubObject(subname, retType, mat, transform, depth)
|
|
|
|
def getInfo(self, noShape=True):
|
|
return getElementInfo(self.getAssembly().getPartGroup(),
|
|
self.getElementSubname(),False,noShape)
|
|
|
|
|
|
class ViewProviderAsmElement(ViewProviderAsmOnTop):
|
|
_iconName = 'Assembly_Assembly_Element.svg'
|
|
_iconDisabledName = 'Assembly_Assembly_ElementDetached.svg'
|
|
|
|
def __init__(self,vobj):
|
|
vobj.addProperty('App::PropertyBool',
|
|
'ShowCS','','Show coordinate cross')
|
|
vobj.ShapeColor = self.getDefaultColor()
|
|
vobj.PointColor = self.getDefaultColor()
|
|
vobj.LineColor = self.getDefaultColor()
|
|
vobj.Transparency = 50
|
|
vobj.LineWidth = 4
|
|
vobj.PointSize = 4
|
|
self.axisNode = None
|
|
self.transNode = None
|
|
super(ViewProviderAsmElement,self).__init__(vobj)
|
|
|
|
def attach(self,vobj):
|
|
super(ViewProviderAsmElement,self).attach(vobj)
|
|
vobj.OnTopWhenSelected = 2
|
|
self.setupAxis()
|
|
|
|
def getDefaultColor(self):
|
|
return (60.0/255.0,1.0,1.0)
|
|
|
|
def canDropObjectEx(self,_obj,owner,subname,elements):
|
|
if not owner:
|
|
return False
|
|
if not elements and not utils.isElement((owner,subname)):
|
|
return False
|
|
proxy = self.ViewObject.Object.Proxy
|
|
return proxy.getAssembly().getPartGroup()==owner
|
|
|
|
def dropObjectEx(self,vobj,_obj,owner,subname,elements):
|
|
if not elements:
|
|
elements = ['']
|
|
for element in elements:
|
|
AsmElement.make(AsmElement.Selection(
|
|
SelObj=None, SelSubname=None, Element=vobj.Object,
|
|
Group=owner, Subname=subname+element),undo=True)
|
|
return '.'
|
|
|
|
def doubleClicked(self,_vobj=None):
|
|
from . import mover
|
|
return mover.movePart(element=self.ViewObject.Object, moveElement=False)
|
|
|
|
def getIcon(self):
|
|
return utils.getIcon(self.__class__,
|
|
getattr(self.ViewObject.Object,'Detach',False))
|
|
|
|
def updateData(self,_obj,prop):
|
|
vobj = getattr(self,'ViewObject',None)
|
|
if not vobj or FreeCAD.isRestoring():
|
|
return
|
|
if prop == 'Detach':
|
|
vobj.signalChangeIcon()
|
|
elif prop in ('Placement','Shape','Radius'):
|
|
self.setupAxis()
|
|
|
|
_AxisOrigin = None
|
|
|
|
def showCS(self):
|
|
vobj = getattr(self,'ViewObject',None)
|
|
if not vobj or hasProperty(vobj.Object,'Radius'):
|
|
return
|
|
if getattr(vobj,'ShowCS',False) or\
|
|
gui.AsmCmdManager.ShowElementCS or\
|
|
not hasattr(vobj.Object,'Shape'):
|
|
return True
|
|
return utils.isInfinite(vobj.Object.Shape)
|
|
|
|
def getElementPicked(self,pp):
|
|
vobj = self.ViewObject
|
|
if self.showCS():
|
|
axis = self._AxisOrigin
|
|
if axis:
|
|
sub = axis.getElementPicked(pp)
|
|
if sub:
|
|
return sub
|
|
return vobj.getElementPicked(pp)
|
|
|
|
def getDetailPath(self,subname,path,append):
|
|
vobj = self.ViewObject
|
|
if subname in ('X', 'Y', 'Z'):
|
|
subname = ''
|
|
return vobj.getDetailPath(subname,path,append)
|
|
|
|
@classmethod
|
|
def getAxis(cls):
|
|
axis = cls._AxisOrigin
|
|
if not axis:
|
|
axis = FreeCADGui.AxisOrigin()
|
|
axis.Labels = {'X':'','Y':'','Z':''}
|
|
cls._AxisOrigin = axis
|
|
return axis.Node
|
|
|
|
def setupAxis(self):
|
|
vobj = getattr(self,'ViewObject', None)
|
|
if not vobj:
|
|
return
|
|
switch = getattr(self,'axisNode',None)
|
|
if not self.showCS():
|
|
if switch:
|
|
switch.whichChild = -1
|
|
return
|
|
|
|
if not switch:
|
|
parentSwitch = vobj.SwitchNode
|
|
if not parentSwitch.getNumChildren():
|
|
return
|
|
|
|
from pivy import coin
|
|
switch = coin.SoSwitch()
|
|
node = coin.SoType.fromName('SoFCSelectionRoot').createInstance()
|
|
switch.addChild(node)
|
|
trans = coin.SoTransform()
|
|
node.addChild(trans)
|
|
node.addChild(ViewProviderAsmElement.getAxis())
|
|
self.axisNode = switch
|
|
self.transNode = trans
|
|
for i in range(parentSwitch.getNumChildren()):
|
|
parentSwitch.getChild(i).addChild(switch)
|
|
|
|
switch.whichChild = 0
|
|
|
|
pla = vobj.Object.Placement.inverse().multiply(
|
|
utils.getElementPlacement(vobj.Object.Shape))
|
|
self.transNode.translation.setValue(pla.Base)
|
|
self.transNode.rotation.setValue(pla.Rotation.Q)
|
|
|
|
def onChanged(self,_vobj,prop):
|
|
if prop == 'ShowCS':
|
|
self.setupAxis()
|
|
|
|
@staticmethod
|
|
def setupMenu(menu, vobj, vobj2):
|
|
obj = vobj.Object
|
|
|
|
action = QtGui.QAction(QtGui.QIcon(), 'Move part', menu)
|
|
action.setToolTip('Move the owner part using this element as reference coordinate')
|
|
QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),vobj2.Proxy.doubleClicked)
|
|
menu.addAction(action)
|
|
|
|
action = QtGui.QAction(QtGui.QIcon(),
|
|
"Attach element" if obj.Detach else "Detach element", menu)
|
|
if obj.Detach:
|
|
action.setToolTip('Attach this element to its linked geometry,\n'
|
|
'so that it will auto update on change.')
|
|
else:
|
|
action.setToolTip('Detach this element so that it stays the same\n'
|
|
'on change of the linked geometry.')
|
|
QtCore.QObject.connect(
|
|
action,QtCore.SIGNAL("triggered()"),vobj.Proxy.toggleDetach)
|
|
menu.addAction(action)
|
|
|
|
if obj.Proxy.isBroken():
|
|
action = QtGui.QAction(QtGui.QIcon(), "Fix element", menu)
|
|
action.setToolTip('Auto fix broken element')
|
|
QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),vobj.Proxy.fix)
|
|
menu.addAction(action)
|
|
|
|
action = QtGui.QAction(QtGui.QIcon(), 'Offset element', menu)
|
|
action.setToolTip('Activate dragger to offset this element')
|
|
menu.addAction(action)
|
|
QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),vobj2.Proxy.offset)
|
|
|
|
if vobj2.Object.Offset != FreeCAD.Placement():
|
|
action = QtGui.QAction(QtGui.QIcon(), 'Reset offset', menu)
|
|
action.setToolTip('Clear offset of this element')
|
|
menu.addAction(action)
|
|
QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),vobj2.Proxy.resetOffset)
|
|
|
|
action = QtGui.QAction(QtGui.QIcon(), 'Flip element', menu)
|
|
action.setToolTip('Flip this element\' Z normal by rotating 180 degree\n'
|
|
'along the X axis (or Y axis by holding the CTRL key).\n\n'
|
|
'Note that depending on the type of constraint and the\n'
|
|
'order of the selected element, flipping element may not\n'
|
|
'be effective. You can try "Flip part" instead.')
|
|
menu.addAction(action)
|
|
QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),vobj2.Proxy.flip)
|
|
|
|
action = QtGui.QAction(QtGui.QIcon(), 'Flip part', menu)
|
|
action.setToolTip('Flip the owner part using this element Z normal as\n' \
|
|
'reference, which is done by rotating 180 degree along\n' \
|
|
'the element\'s X axis (or Y axis by holding the CTRL key).\n\n'
|
|
'Note that depending on the type of constraint and the\n'
|
|
'order of the selected element, flipping part may not\n'
|
|
'be effective. You can try "Flip element" instead.')
|
|
menu.addAction(action)
|
|
QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),vobj2.Proxy.flipPart)
|
|
|
|
|
|
def setupContextMenu(self,vobj,menu):
|
|
ViewProviderAsmElement.setupMenu(menu, vobj, vobj)
|
|
return True
|
|
|
|
def fix(self):
|
|
obj = self.ViewObject.Object
|
|
FreeCAD.setActiveTransaction('Fix element')
|
|
try:
|
|
obj.Proxy.fix()
|
|
obj.recompute();
|
|
FreeCAD.closeActiveTransaction()
|
|
except Exception:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
|
|
def toggleDetach(self):
|
|
obj = self.ViewObject.Object
|
|
FreeCAD.setActiveTransaction('Attach element' if obj.Detach else 'Detach element')
|
|
try:
|
|
obj.Detach = not obj.Detach
|
|
FreeCAD.closeActiveTransaction()
|
|
except Exception:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
|
|
def offset(self):
|
|
from . import mover
|
|
return mover.movePart(element=self.ViewObject.Object, moveElement=True)
|
|
|
|
@staticmethod
|
|
def doResetOffset(obj):
|
|
FreeCAD.setActiveTransaction('Reset offset')
|
|
obj.Offset = FreeCAD.Placement()
|
|
obj.recompute(True)
|
|
FreeCAD.closeActiveTransaction()
|
|
|
|
def resetOffset(self):
|
|
obj = self.ViewObject.Object
|
|
ViewProviderAsmElement.doResetOffset(obj)
|
|
|
|
@staticmethod
|
|
def doFlip(obj, info, flipElement):
|
|
if QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ControlModifier:
|
|
rot = FreeCAD.Rotation(FreeCAD.Vector(0,1,0),180)
|
|
else:
|
|
rot = FreeCAD.Rotation(FreeCAD.Vector(1,0,0),180)
|
|
rot = FreeCAD.Placement(FreeCAD.Vector(), rot)
|
|
|
|
FreeCAD.setActiveTransaction(
|
|
'Flip element' if flipElement else 'Flip part')
|
|
try:
|
|
if flipElement:
|
|
obj.Offset = rot.multiply(obj.Offset)
|
|
else:
|
|
offset = utils.getElementPlacement(obj.getSubObject(''))
|
|
offset = offset.multiply(rot).multiply(offset.inverse())
|
|
setPlacement(info.Part, offset.multiply(info.Placement))
|
|
obj.recompute(True)
|
|
FreeCAD.closeActiveTransaction()
|
|
except Exception:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
|
|
def flip(self):
|
|
obj = self.ViewObject.Object
|
|
ViewProviderAsmElement.doFlip(obj, obj.Proxy.getInfo(), True)
|
|
|
|
def flipPart(self):
|
|
obj = self.ViewObject.Object
|
|
ViewProviderAsmElement.doFlip(obj, obj.Proxy.getInfo(), False)
|
|
|
|
def getLinkedViewProvider(self, recursive):
|
|
obj = self.ViewObject.Object
|
|
try:
|
|
sub = obj.Proxy.getElementSubname(recursive)
|
|
except Exception:
|
|
return
|
|
linked = obj.Proxy.getAssembly().getPartGroup().getSubObject(sub, retType=1)
|
|
if not linked:
|
|
return
|
|
subs = Part.splitSubname(sub)
|
|
if subs[1] or subs[2]:
|
|
return (linked.ViewObject, Part.joinSubname('', subs[1], subs[2]))
|
|
return linked.ViewObject
|
|
|
|
class AsmElementSketch(AsmElement):
|
|
def __init__(self,obj,parent):
|
|
super(AsmElementSketch,self).__init__(parent)
|
|
obj.Proxy = self
|
|
self.attach(obj)
|
|
|
|
def linkSetup(self,obj):
|
|
super(AsmElementSketch,self).linkSetup(obj)
|
|
obj.setPropertyStatus('Placement',('Hidden','-Immutable'))
|
|
|
|
@classmethod
|
|
def create(cls,name,parent):
|
|
element = parent.Document.addObject("Part::FeaturePython", name)
|
|
cls(element,parent)
|
|
ViewProviderAsmElementSketch(element.ViewObject)
|
|
return element
|
|
|
|
def execute(self,obj):
|
|
shape = utils.getElementShape(obj.LinkedObject)
|
|
obj.Placement = shape.Placement
|
|
obj.Shape = shape
|
|
return False
|
|
|
|
def getSubObject(self,obj,subname,retType,mat,transform,depth):
|
|
link = obj.LinkedObject
|
|
if isinstance(link,tuple) and \
|
|
(not subname or subname==link[1]):
|
|
ret = link[0].getSubObject(subname,retType,mat,transform,depth+1)
|
|
if ret == link[0]:
|
|
ret = obj
|
|
elif isinstance(ret,(tuple,list)):
|
|
ret = list(ret)
|
|
ret[0] = obj
|
|
return ret
|
|
|
|
|
|
class ViewProviderAsmElementSketch(ViewProviderAsmElement):
|
|
def getIcon(self):
|
|
return ":/icons/Sketcher_Sketch.svg"
|
|
|
|
def getDetail(self,_name):
|
|
pass
|
|
|
|
def getElement(self,_det):
|
|
link = self.ViewObject.Object.LinkedObject
|
|
if isinstance(link,tuple):
|
|
subs = link[1].split('.')
|
|
if subs:
|
|
return subs[-1]
|
|
return ''
|
|
|
|
def updateData(self,obj,prop):
|
|
_ = obj
|
|
_ = prop
|
|
|
|
|
|
ElementInfo = namedtuple('AsmElementInfo', ('Parent','SubnameRef','Part',
|
|
'PartName','Placement','Object','Subname','Shape'))
|
|
|
|
def getElementInfo(parent,subname,
|
|
checkPlacement=False,shape=None,recursive=False):
|
|
'''Return a named tuple containing the part object element information
|
|
|
|
Parameters:
|
|
|
|
parent: the parent document object, either an assembly, or a part group
|
|
|
|
subname: subname reference to the part element (i.e. edge, face, vertex)
|
|
|
|
shape: caller can pass in a pre-obtained element shape. The shape is
|
|
assumed to be in the assembly coordinate space. This function will then
|
|
transform the shape into the its owner part's coordinate space. If
|
|
'shape' is not given, then the output shape will be obtained through
|
|
'parent' and 'subname'
|
|
|
|
Return a named tuple with the following fields:
|
|
|
|
Parent: set to the input parent object
|
|
|
|
SubnameRef: set to the input subname reference
|
|
|
|
Part: either the part object, or a tuple(array,idx,element,collapsed) to
|
|
refer to an element in an link array,
|
|
|
|
PartName: a string name for the part
|
|
|
|
Placement: the placement of the part
|
|
|
|
Object: the object that owns the element. In case 'Part' is an assembly, the
|
|
element owner will always be some (grand)child of the 'Part'
|
|
|
|
Subname: the subname reference to the element owner object. The reference is
|
|
relative to the 'Part', i.e. Object = Part.getSubObject(subname), or if
|
|
'Part' is a tuple, Object = Part[0].getSubObject(str(Part[1]) + '.' +
|
|
subname)
|
|
|
|
Shape: Part.Shape of the linked element. The shape's placement is relative
|
|
to the owner Part.
|
|
'''
|
|
|
|
subnameRef = subname
|
|
parentSave = parent
|
|
|
|
if isTypeOf(parent,Assembly,True):
|
|
idx = subname.index('.')
|
|
parent = parent.getSubObject(subname[:idx+1],1)
|
|
subname = subname[idx+1:]
|
|
|
|
if isTypeOf(parent,(AsmElementGroup,AsmConstraintGroup)):
|
|
child = parent.getSubObject(subname,1)
|
|
if not isTypeOf(child,(AsmElement,AsmElementLink)):
|
|
raise RuntimeError('Invalid sub-object {}, {}'.format(
|
|
objName(parent), subname))
|
|
subname = child.Proxy.getElementSubname(recursive)
|
|
partGroup = parent.Proxy.getAssembly().getPartGroup()
|
|
|
|
elif isTypeOf(parent,AsmPartGroup):
|
|
partGroup = parent
|
|
else:
|
|
raise RuntimeError('{} is not Assembly or PartGroup'.format(
|
|
objName(parent)))
|
|
|
|
subname = flattenSubname(partGroup,subname)
|
|
names = subname.split('.')
|
|
part = partGroup.getSubObject(names[0]+'.',1)
|
|
if not part:
|
|
raise RuntimeError('Invalid sub-object {}, {}'.format(
|
|
objName(parent), subnameRef))
|
|
partSaved = part
|
|
|
|
transformShape = True if isinstance(shape,Part.Shape) else False
|
|
|
|
# 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):
|
|
|
|
# special treatment of link array (i.e. when ElementCount!=0), we
|
|
# allow the array element to be moveable by the solver
|
|
if getLinkProperty(part,'ElementCount'):
|
|
|
|
# Handle old element reference before this link is expanded to
|
|
# array.
|
|
if not names[1]:
|
|
names[1] = '0'
|
|
names.append('')
|
|
elif len(names) == 2:
|
|
names.insert(1,'0')
|
|
|
|
# store both the part (i.e. the link array), and the array
|
|
# element object
|
|
part = (part,part.getSubObject(names[1]+'.',1))
|
|
if not part[1]:
|
|
raise RuntimeError('Cannot find part array element {}.{}.',
|
|
part.Name,names[1])
|
|
|
|
# trim the subname to be after the array element
|
|
subname = '.'.join(names[2:])
|
|
if not shape:
|
|
shape=utils.getElementShape((part[1],subname))
|
|
|
|
# There are two states of an link array.
|
|
if getLinkProperty(part[0],'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.
|
|
pla = part[0].Placement.multiply(part[1].Placement)
|
|
obj = part[1].getLinkedObject(False)
|
|
partName = objName(part[1])
|
|
idx = int(partName.split('_i')[-1])
|
|
part = (part[0],idx,part[1],False)
|
|
else:
|
|
plaList = getLinkProperty(part[0],'PlacementList',None,True)
|
|
if plaList:
|
|
# b) The elements are collapsed. Then the moveable Placement
|
|
# is stored inside link object's PlacementList property.
|
|
obj = part[1]
|
|
try:
|
|
if names[1] == part[1].Name:
|
|
idx = 0
|
|
else:
|
|
idx = int(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],idx,part[1],True)
|
|
pla = part[0].Placement.multiply(plaList[idx])
|
|
except ValueError:
|
|
raise RuntimeError('invalid array subname of element '
|
|
'{}: {}'.format(objName(parent),subnameRef))
|
|
|
|
partName = '{}.{}.'.format(objName(part[0]),idx)
|
|
|
|
if not obj:
|
|
part = partSaved
|
|
# Here means, either the 'part' is an assembly or it is a non array
|
|
# object. We trim the subname reference to be relative to the part
|
|
# object. And obtain the shape before part's Placement by setting
|
|
# 'transform' to False
|
|
if checkPlacement and not hasProperty(part,'Placement'):
|
|
raise RuntimeError('part has no placement')
|
|
subname = '.'.join(names[1:])
|
|
if not shape:
|
|
shape = utils.getElementShape((part,subname))
|
|
if not shape:
|
|
raise RuntimeError('Failed to get geometry element from '
|
|
'{}.{}'.format(objName(part),subname))
|
|
pla = getattr(part,'Placement',FreeCAD.Placement())
|
|
obj = part.getLinkedObject(False)
|
|
partName = part.Name
|
|
|
|
if transformShape:
|
|
# Copy and transform shape. We have to copy the shape here to work
|
|
# around of obscure OCCT edge transformation bug
|
|
shape.transformShape(pla.toMatrix().inverse(),True)
|
|
|
|
return ElementInfo(Parent = parentSave,
|
|
SubnameRef = subnameRef,
|
|
Part = part,
|
|
PartName = partName,
|
|
Placement = pla.copy(),
|
|
Object = obj,
|
|
Subname = subname,
|
|
Shape = shape)
|
|
|
|
|
|
class AsmElementLink(AsmBase):
|
|
def __init__(self,parent):
|
|
super(AsmElementLink,self).__init__()
|
|
self.version = None
|
|
self.info = None
|
|
self.infos = []
|
|
self.part = None
|
|
self.parent = getProxy(parent,AsmConstraint)
|
|
self.multiply = False
|
|
|
|
def linkSetup(self,obj):
|
|
super(AsmElementLink,self).linkSetup(obj)
|
|
parent = getattr(obj,'_Parent',None)
|
|
if parent:
|
|
self.parent = parent.Proxy
|
|
obj.setPropertyStatus('LinkedObject','ReadOnly')
|
|
if not hasProperty(obj,'Offset'):
|
|
obj.addProperty("App::PropertyPlacement","Offset"," Link",'')
|
|
if not hasProperty(obj,'Placement'):
|
|
obj.addProperty("App::PropertyPlacement","Placement"," Link",'')
|
|
obj.setPropertyStatus('Placement','Hidden')
|
|
if not hasProperty(obj,'LinkTransform'):
|
|
obj.addProperty("App::PropertyBool","LinkTransform"," Link",'')
|
|
obj.LinkTransform = True
|
|
obj.setPropertyStatus('LinkTransform',['Immutable','Hidden'])
|
|
obj.configLinkProperty('LinkedObject','Placement','LinkTransform')
|
|
if hasProperty(obj,'Count'):
|
|
obj.configLinkProperty(ElementCount='Count')
|
|
if hasProperty(obj,'PlacementList'):
|
|
obj.configLinkProperty('PlacementList')
|
|
if hasProperty(obj,'ShowElement'):
|
|
obj.configLinkProperty('ShowElement')
|
|
self.info = None
|
|
self.infos = []
|
|
self.part = None
|
|
self.multiply = False
|
|
|
|
self.version = AsmVersion()
|
|
|
|
# Suppress link array (when ShowElement=True) getSubObjects, so that view
|
|
# provider getBoundingBox can work.
|
|
def getSubObjects(self, _obj, _reason):
|
|
return
|
|
|
|
def migrate(self,obj):
|
|
link = obj.LinkedObject
|
|
if not isinstance(link,tuple):
|
|
return
|
|
touched = 'Touched' in obj.State
|
|
if isTypeOf(link[0],(AsmPartGroup,AsmElementGroup)):
|
|
owner = link[0]
|
|
subname = link[1]
|
|
else:
|
|
owner = self.getAssembly().getPartGroup()
|
|
subname = '{}.{}'.format(link[0].Name,link[1])
|
|
logger.catchDebug('migrate ElementLink',self.setLink,owner,subname)
|
|
if not touched:
|
|
obj.purgeTouched()
|
|
|
|
def childVersion(self,linked,mat):
|
|
if not isTypeOf(linked,AsmElement):
|
|
return None
|
|
obj = self.Object
|
|
return (getattr(obj,'Count',0),
|
|
linked,
|
|
linked.Proxy.version.value,
|
|
obj.Offset,
|
|
mat,
|
|
getattr(obj,'PlacementList',None))
|
|
|
|
def attach(self,obj):
|
|
obj.addProperty("App::PropertyXLink","LinkedObject"," Link",'')
|
|
obj.addProperty("App::PropertyLinkHidden","_Parent"," Link",'')
|
|
obj._Parent = self.parent.Object
|
|
obj.setPropertyStatus('_Parent',('Hidden','Immutable'))
|
|
super(AsmElementLink,self).attach(obj)
|
|
|
|
def canLinkProperties(self,_obj):
|
|
return False
|
|
|
|
def allowDuplicateLabel(self,_obj):
|
|
return True
|
|
|
|
def execute(self,obj):
|
|
link = obj.LinkedObject
|
|
if isinstance(link,tuple):
|
|
subname = link[1]
|
|
link = link[0]
|
|
else:
|
|
subname = ''
|
|
linked,mat = link.getSubObject(subname,1,FreeCAD.Matrix())
|
|
if linked and linked.Label != linked.Name:
|
|
obj.Label = linked.Label
|
|
|
|
info = None
|
|
if getattr(obj,'Count',None):
|
|
info = self.getInfo(True)
|
|
|
|
version = self.childVersion(linked,mat)
|
|
if not self.version.update(version):
|
|
logger.debug('skip {}, {}, {}',
|
|
objName(obj),self.version.childVersion,version)
|
|
return
|
|
logger.debug('not skip {}, {}',objName(obj),version)
|
|
|
|
if not info:
|
|
info = self.getInfo(True)
|
|
relationGroup = self.getAssembly().getRelationGroup()
|
|
if relationGroup and (not self.part or self.part!=info.Part):
|
|
oldPart = self.part
|
|
self.part = info.Part
|
|
relationGroup.Proxy.update(
|
|
self.parent.Object,oldPart,info.Part,info.PartName)
|
|
self.version.commit()
|
|
return False
|
|
|
|
_MyIgnoredProperties = _IgnoredProperties | \
|
|
set(('Count','PlacementList'))
|
|
|
|
def onChanged(self,obj,prop):
|
|
if obj.Removing or \
|
|
not getattr(self,'parent',None) or \
|
|
FreeCAD.isRestoring():
|
|
return
|
|
elif obj.Document and getattr(obj.Document,'Transacting',False):
|
|
self.infos *= 0 # clear the list
|
|
self.info = None
|
|
return
|
|
elif prop == 'Count':
|
|
self.infos *= 0 # clear the list
|
|
self.info = None
|
|
return
|
|
elif prop == 'Offset':
|
|
self.getInfo(True)
|
|
return
|
|
elif prop == 'NoExpand':
|
|
cstr = self.parent.Object
|
|
if obj!=cstr.Group[0] \
|
|
and cstr.Multiply \
|
|
and obj.LinkedObject:
|
|
self.setLink(self.getAssembly().getPartGroup(),
|
|
self.getElementSubname(True))
|
|
return
|
|
elif prop == 'Label':
|
|
if obj.Document and getattr(obj.Document,'Transacting',False):
|
|
return
|
|
link = getattr(obj,'LinkedObject',None)
|
|
if isinstance(link,tuple):
|
|
linked = link[0].getSubObject(link[1],1)
|
|
else:
|
|
linked = link
|
|
if linked and linked.Label != obj.Label:
|
|
linked.Label = obj.Label
|
|
# in case there is label duplication, AsmElement will auto
|
|
# re-lable it.
|
|
obj.Label = linked.Label
|
|
return
|
|
elif prop == 'AutoCount':
|
|
if obj.AutoCount and hasProperty(obj,'ShowElement'):
|
|
self.parent.checkMultiply()
|
|
if prop not in self._MyIgnoredProperties and \
|
|
not Constraint.isDisabled(self.parent.Object):
|
|
Assembly.autoSolve(obj,prop)
|
|
|
|
def getAssembly(self):
|
|
return self.parent.parent.parent
|
|
|
|
def getElementSubname(self,recursive=False):
|
|
'Resolve element link subname'
|
|
|
|
# AsmElementLink is used by constraint to link to a geometry link. It
|
|
# does so by indirectly linking to an AsmElement object belonging to
|
|
# the same parent or child assembly. AsmElement is also a link, which
|
|
# again links to another AsmElement of a child assembly or the actual
|
|
# geometry element of a child feature. This function is for resolving
|
|
# the AsmElementLink's subname reference to the actual part object
|
|
# subname reference relative to the parent assembly's part group
|
|
|
|
link = self.Object.LinkedObject
|
|
if not isinstance(link,tuple):
|
|
linked = link
|
|
else:
|
|
linked = link[0].getSubObject(link[1],1)
|
|
if not linked:
|
|
raise RuntimeError('broken link')
|
|
element = getProxy(linked,AsmElement)
|
|
assembly = element.getAssembly()
|
|
if assembly == self.getAssembly():
|
|
return element.getElementSubname(recursive)
|
|
|
|
# The reference is stored inside this ElementLink. We need the
|
|
# sub-assembly name, which is the name before the first dot. This name
|
|
# may be different from the actual assembly object's name, in case where
|
|
# the assembly is accessed through a link. And the sub-assembly may be
|
|
# inside a link array, which we don't know for sure. But we do know that
|
|
# the last two names are element group and element label. So just pop
|
|
# two names. The -3 below is to account for the last ending '.'
|
|
ref = [link[0].Name] + link[1].split('.')[:-3]
|
|
return '{}.2.{}'.format('.'.join(ref),
|
|
element.getElementSubname(recursive))
|
|
|
|
def setLink(self,owner,subname,checkOnly=False,multiply=False):
|
|
obj = self.Object
|
|
cstr = self.parent.Object
|
|
elements = flattenGroup(cstr)
|
|
radius = None
|
|
if (multiply or Constraint.canMultiply(cstr)) and \
|
|
obj!=elements[0] and \
|
|
not getattr(obj,'NoExpand',None):
|
|
|
|
info = getElementInfo(owner,subname)
|
|
|
|
radius = utils.getElementCircular(info.Shape,True)
|
|
if radius and not checkOnly and not hasProperty(obj,'NoExpand'):
|
|
touched = 'Touched' in obj.State
|
|
obj.addProperty('App::PropertyBool','NoExpand','',
|
|
'Disable auto inclusion of coplanar edges '\
|
|
'with the same radius')
|
|
if len(elements)>2 and getattr(elements[-2],'NoExpand',None):
|
|
obj.NoExpand = True
|
|
radius = None
|
|
if not touched:
|
|
obj.purgeTouched()
|
|
if radius:
|
|
if isinstance(info.Part,tuple):
|
|
parentShape = Part.getShape(info.Part[2], info.Subname,
|
|
transform=info.Part[3], needSubElement=False)
|
|
else:
|
|
parentShape = Part.getShape(info.Part, info.Subname,
|
|
transform=False, needSubElement=False)
|
|
count = 0
|
|
for edge in parentShape.Edges:
|
|
if not info.Shape.isCoplanar(edge) or \
|
|
not utils.isSameValue(
|
|
utils.getElementCircular(edge,True),radius):
|
|
continue
|
|
count += 1
|
|
if count > 1:
|
|
break
|
|
if count<=1:
|
|
radius = None
|
|
|
|
if checkOnly:
|
|
return True
|
|
|
|
#####################################################################
|
|
# Note: we no longer link directly to sub-assembly's Element any more.
|
|
# Instead, We always link through local element, to make it easy for
|
|
# user to recover missing elements in case it happens
|
|
#####################################################################
|
|
|
|
sel = AsmElement.Selection(SelObj=None, SelSubname=None,
|
|
Element=None, Group=owner, Subname=subname)
|
|
element = AsmElement.make(sel,radius=radius,name='_Element')
|
|
|
|
for sibling in elements:
|
|
if sibling == obj:
|
|
continue
|
|
if sibling.LinkedObject == element:
|
|
raise RuntimeError('duplicate element link {} in constraint '
|
|
'{}'.format(objName(sibling),objName(cstr)))
|
|
obj.setLink(element)
|
|
if obj.Label!=obj.Name and element.Label.startswith('_Element'):
|
|
if not obj.Label.startswith('_'):
|
|
element.Label = '_' + obj.Label
|
|
else:
|
|
element.Label = obj.Label
|
|
obj.Label = element.Label
|
|
|
|
def getInfo(self,refresh=False,expand=False):
|
|
if not refresh and self.info is not None:
|
|
return self.infos if expand else self.info
|
|
|
|
self.info = None
|
|
self.infos = []
|
|
obj = getattr(self,'Object',None)
|
|
if not obj:
|
|
return
|
|
|
|
linked = obj.LinkedObject
|
|
if isinstance(linked,tuple):
|
|
subname = linked[1]
|
|
linked = linked[0]
|
|
else:
|
|
subname = ''
|
|
shape = Part.getShape(linked,subname,
|
|
needSubElement=True,noElementMap=True)
|
|
self.info = getElementInfo(self.getAssembly().getPartGroup(),
|
|
self.getElementSubname(),shape=shape)
|
|
info = self.info
|
|
|
|
if obj.Offset.isIdentity():
|
|
if not obj.Placement.isIdentity():
|
|
obj.Placement = FreeCAD.Placement()
|
|
else:
|
|
# obj.Offset is in the element shape's coordinate system, we need to
|
|
# transform it to the assembly coordinate system
|
|
mShape = utils.getElementPlacement(info.Shape).toMatrix()
|
|
mOffset = obj.Offset.toMatrix()
|
|
mat = info.Placement.toMatrix()*mShape
|
|
pla = FreeCAD.Placement(mat*mOffset*mat.inverse())
|
|
if not utils.isSamePlacement(obj.Placement,pla):
|
|
obj.Placement = pla
|
|
info.Shape.transformShape(mShape*mOffset*mShape.inverse())
|
|
|
|
info = ElementInfo(Parent = info.Parent,
|
|
SubnameRef = info.SubnameRef,
|
|
Part = info.Part,
|
|
PartName = info.PartName,
|
|
Placement = info.Placement,
|
|
Object = info.Object,
|
|
Subname = '{}.{}'.format(
|
|
info.Subname,hash(str(obj.Offset))),
|
|
Shape = info.Shape)
|
|
self.info = info
|
|
|
|
parent = self.parent.Object
|
|
if not Constraint.canMultiply(parent):
|
|
self.multiply = False
|
|
self.infos.append(info)
|
|
return self.infos if expand else self.info
|
|
|
|
self.multiply = True
|
|
if obj == parent.Group[0]:
|
|
if not isinstance(info.Part,tuple) or \
|
|
getLinkProperty(info.Part[0],'ElementCount')!=obj.Count:
|
|
self.infos.append(info)
|
|
return self.infos if expand else self.info
|
|
infos = []
|
|
offset = info.Placement.inverse()
|
|
plaList = []
|
|
for i in range(obj.Count):
|
|
part = info.Part
|
|
if part[3]:
|
|
pla = getLinkProperty(part[0],'PlacementList')[i]
|
|
part = (part[0],i,part[2],part[3])
|
|
else:
|
|
sobj = part[0].getSubObject(str(i)+'.',1)
|
|
pla = sobj.Placement
|
|
part = (part[0],i,sobj,part[3])
|
|
pla = part[0].Placement.multiply(pla)
|
|
plaList.append(pla.multiply(offset))
|
|
infos.append(ElementInfo(
|
|
Parent = info.Parent,
|
|
SubnameRef = info.SubnameRef,
|
|
Part=part,
|
|
PartName = '{}.{}'.format(objName(part[0]),i),
|
|
Placement = pla,
|
|
Object = info.Object,
|
|
Subname = info.Subname,
|
|
Shape = info.Shape))
|
|
obj.PlacementList = plaList
|
|
self.infos = infos
|
|
return infos if expand else info
|
|
|
|
for i,edge in enumerate(info.Shape.Edges):
|
|
self.infos.append(ElementInfo(
|
|
Parent = info.Parent,
|
|
SubnameRef = info.SubnameRef,
|
|
Part = info.Part,
|
|
PartName = info.PartName,
|
|
Placement = info.Placement,
|
|
Object = info.Object,
|
|
Subname = '{}_{}'.format(info.Subname,i),
|
|
Shape = edge))
|
|
|
|
return self.infos if expand else self.info
|
|
|
|
MakeInfo = namedtuple('AsmElementLinkMakeInfo',
|
|
('Constraint','Owner','Subname'))
|
|
|
|
@staticmethod
|
|
def make(info,name='ElementLink'):
|
|
link = info.Constraint.Document.addObject("App::FeaturePython",
|
|
name,AsmElementLink(info.Constraint),None,True)
|
|
ViewProviderAsmElementLink(link.ViewObject)
|
|
info.Constraint.setLink({-1:link})
|
|
link.Proxy.setLink(info.Owner,info.Subname)
|
|
if gui.AsmCmdManager.AutoElementVis:
|
|
info.Constraint.setElementVisible(link.Name,False)
|
|
return link
|
|
|
|
|
|
def setPlacement(part,pla,purgeTouched=False):
|
|
''' called by solver after solving to adjust the placement.
|
|
|
|
part: obtained by AsmConstraint.getInfo().Part pla: the new placement
|
|
pla: new placement
|
|
purgeTouched: set to True to not touch object
|
|
'''
|
|
if not isinstance(part,tuple):
|
|
if purgeTouched:
|
|
obj = part
|
|
touched = 'Touched' in obj.State
|
|
part.Placement = pla
|
|
else:
|
|
pla = part[0].Placement.inverse().multiply(pla)
|
|
if part[3]:
|
|
if purgeTouched:
|
|
obj = part[0]
|
|
touched = 'Touched' in obj.State
|
|
setLinkProperty(part[0],'PlacementList',{part[1]:pla})
|
|
else:
|
|
if purgeTouched:
|
|
obj = part[2]
|
|
touched = 'Touched' in obj.State
|
|
part[2].Placement = pla
|
|
if purgeTouched and not touched:
|
|
obj.purgeTouched()
|
|
|
|
|
|
def showPart(partGroup,part,show=True,purgeTouched=True):
|
|
if not isinstance(part,tuple):
|
|
parent = partGroup
|
|
name = part.Name
|
|
else:
|
|
parent = part[0]
|
|
name = str(part[1])
|
|
if purgeTouched:
|
|
touched = 'Touched' in parent.State
|
|
parent.setElementVisible(name,show)
|
|
if purgeTouched and not touched:
|
|
parent.purgeTouched()
|
|
|
|
|
|
class ViewProviderAsmElementLink(ViewProviderAsmOnTop):
|
|
def __init__(self,vobj):
|
|
vobj.OverrideMaterial = True
|
|
vobj.ShapeMaterial.DiffuseColor = self.getDefaultColor()
|
|
vobj.ShapeMaterial.EmissiveColor = self.getDefaultColor()
|
|
super(ViewProviderAsmElementLink,self).__init__(vobj)
|
|
|
|
def attach(self,vobj):
|
|
super(ViewProviderAsmElementLink,self).attach(vobj)
|
|
vobj.OnTopWhenSelected = 2
|
|
|
|
def claimChildren(self):
|
|
return []
|
|
|
|
def getDefaultColor(self):
|
|
return (1.0,60.0/255.0,60.0/255.0)
|
|
|
|
def doubleClicked(self,_vobj=None):
|
|
from . import mover
|
|
return mover.movePart(element=self.ViewObject.Object, moveElement=False)
|
|
|
|
def canDropObjectEx(self,_obj,owner,subname,elements):
|
|
if len(elements)>1 or not owner:
|
|
return False
|
|
elif elements:
|
|
subname += elements[0]
|
|
me = self.ViewObject.Object
|
|
msg = 'Cannot drop to AsmElementLink {}'.format(objName(me))
|
|
if logger.catchTrace(msg, me.Proxy.setLink,owner,subname,True):
|
|
return True
|
|
return False
|
|
|
|
def dropObjectEx(self,vobj,_obj,owner,subname,elements):
|
|
if len(elements)>1:
|
|
return
|
|
elif elements:
|
|
subname += elements[0]
|
|
vobj.Object.Proxy.setLink(owner,subname)
|
|
return '.'
|
|
|
|
def setupContextMenu(self,vobj,menu):
|
|
element = vobj.Object.LinkedObject
|
|
if not isTypeOf(element, AsmElement):
|
|
return;
|
|
|
|
ViewProviderAsmElement.setupMenu(menu, element.ViewObject, vobj)
|
|
return True
|
|
|
|
def offset(self):
|
|
from . import mover
|
|
return mover.movePart(element=self.ViewObject.Object, moveElement=True)
|
|
|
|
def resetOffset(self):
|
|
obj = self.ViewObject.Object
|
|
ViewProviderAsmElement.doResetOffset(obj)
|
|
|
|
def flip(self):
|
|
obj = self.ViewObject.Object
|
|
ViewProviderAsmElement.doFlip(obj, obj.Proxy.getInfo(), True)
|
|
|
|
def flipPart(self):
|
|
obj = self.ViewObject.Object
|
|
ViewProviderAsmElement.doFlip(obj, obj.Proxy.getInfo(), False)
|
|
|
|
def getLinkedViewProvider(self, recursive):
|
|
obj = self.ViewObject.Object
|
|
if not recursive:
|
|
return obj.LinkedObject.ViewObject
|
|
try:
|
|
sub = obj.Proxy.getElementSubname(True)
|
|
except Exception:
|
|
return
|
|
linked = obj.Proxy.getAssembly().getPartGroup().getSubObject(sub, retType=1)
|
|
if not linked:
|
|
return
|
|
subs = Part.splitSubname(sub)
|
|
if subs[1] or subs[2]:
|
|
return (linked.ViewObject, Part.joinSubname('',subs[1], subs[2]))
|
|
return linked.ViewObject
|
|
|
|
|
|
class AsmConstraint(AsmGroup):
|
|
|
|
def __init__(self,parent):
|
|
self.prevOrder = []
|
|
self.version = None
|
|
self._initializing = True
|
|
self.elements = None
|
|
self.parent = getProxy(parent,AsmConstraintGroup)
|
|
super(AsmConstraint,self).__init__()
|
|
|
|
def getAssembly(self):
|
|
return self.parent.parent
|
|
|
|
def checkSupport(self):
|
|
# this function maybe called during document restore, hence the
|
|
# extensive check below
|
|
obj = getattr(self,'Object',None)
|
|
if not obj:
|
|
return
|
|
if Constraint.isDisabled(obj):
|
|
return
|
|
parent = getattr(self,'parent',None)
|
|
if not parent:
|
|
return
|
|
parent = getattr(parent,'parent',None)
|
|
if not parent:
|
|
return
|
|
assembly = getattr(parent,'Object',None)
|
|
if not assembly or \
|
|
System.isConstraintSupported(assembly,Constraint.getTypeName(obj)):
|
|
return
|
|
logger.error('Constraint type "{}" is not supported by '
|
|
'solver "{}"',Constraint.getTypeName(obj),
|
|
System.getTypeName(assembly))
|
|
Constraint.setDisable(obj)
|
|
|
|
def onChanged(self,obj,prop):
|
|
if obj.Document and getattr(obj.Document,'Transacting',False):
|
|
Constraint.onChanged(obj,prop)
|
|
if prop == 'Multiply' and not obj.Multiply:
|
|
children = obj.Group
|
|
if children and hasattr(children[0],'Count'):
|
|
children[0].Count = 0
|
|
return
|
|
if not obj.Removing and prop not in _IgnoredProperties:
|
|
if prop == Constraint.propMultiply() and not FreeCAD.isRestoring():
|
|
self.checkMultiply()
|
|
self.elements = None
|
|
Constraint.onChanged(obj,prop)
|
|
Assembly.autoSolve(obj,prop)
|
|
|
|
def childVersion(self):
|
|
return [(o,o.Proxy.version.value) \
|
|
for o in flattenGroup(self.Object)]
|
|
|
|
def linkSetup(self,obj):
|
|
parent = getattr(obj,'_Parent',None)
|
|
if parent:
|
|
self.parent = parent.Proxy
|
|
self.elements = None
|
|
super(AsmConstraint,self).linkSetup(obj)
|
|
Constraint.attach(obj)
|
|
self.version = AsmVersion()
|
|
|
|
def attach(self,obj):
|
|
obj.addProperty("App::PropertyLinkHidden","_Parent"," Link",'')
|
|
obj._Parent = self.parent.Object
|
|
obj.setPropertyStatus('_Parent',('Hidden','Immutable'))
|
|
super(AsmConstraint,self).attach(obj)
|
|
|
|
def checkMultiply(self):
|
|
obj = self.Object
|
|
if not obj.Multiply:
|
|
return
|
|
|
|
if getattr(obj,'Cascade',False):
|
|
obj.Cascade = False
|
|
|
|
children = obj.Group
|
|
if len(children)<=1:
|
|
return
|
|
count = 0
|
|
shapes = []
|
|
# count the total edges for multiplication
|
|
for e in children[1:]:
|
|
touched = 'Touched' in e.State
|
|
info = e.Proxy.getInfo(not e.Proxy.multiply)
|
|
if not touched:
|
|
e.purgeTouched()
|
|
if info.Shape.countElement('Face'):
|
|
elementCount = 1
|
|
name = 'Face1'
|
|
else:
|
|
elementCount = info.Shape.countElement('Edge')
|
|
name = 'Edge1'
|
|
if not elementCount:
|
|
shapes.append(None)
|
|
e.Proxy.infos = []
|
|
else:
|
|
count += elementCount
|
|
shapes.append(info.Shape.getElement(name))
|
|
|
|
# merge elements that are coplanar
|
|
poses = []
|
|
infos = []
|
|
elements = []
|
|
for i,e in enumerate(children[1:]):
|
|
e.Proxy._refPla = None
|
|
shape = shapes[i]
|
|
if not shape:
|
|
continue
|
|
for j,e2 in enumerate(children[i+2:]):
|
|
shape2 = shapes[i+j+1]
|
|
if not shape2:
|
|
continue
|
|
if shape.isCoplanar(shape2):
|
|
e.Proxy.infos += e2.Proxy.infos
|
|
e2.Proxy.infos = []
|
|
for info in e.Proxy.infos:
|
|
elements.append(e.Proxy)
|
|
infos.append(info)
|
|
poses.append(info.Placement.multVec(
|
|
utils.getElementPos(info.Shape)))
|
|
|
|
# Multiply the part object owning the first element, i.e. change its
|
|
# element count
|
|
|
|
firstChild = children[0]
|
|
info = firstChild.Proxy.getInfo()
|
|
if not isinstance(info.Part,tuple):
|
|
raise RuntimeError('Expect part {} to be an array for '
|
|
'constraint multiplication'.format(info.PartName))
|
|
|
|
touched = 'Touched' in firstChild.State
|
|
if not hasProperty(firstChild,'Count'):
|
|
firstChild.addProperty("App::PropertyInteger","Count",'','')
|
|
firstChild.setPropertyStatus('Count','ReadOnly')
|
|
firstChild.configLinkProperty(ElementCount='Count')
|
|
|
|
if not hasProperty(firstChild,'AutoCount'):
|
|
firstChild.addProperty("App::PropertyBool","AutoCount",'',
|
|
'Auto change part count to match constraining elements')
|
|
firstChild.AutoCount = True
|
|
|
|
if not hasProperty(firstChild,'PlacementList'):
|
|
firstChild.addProperty("App::PropertyPlacementList",
|
|
"PlacementList",'','')
|
|
firstChild.setPropertyStatus('PlacementList','Output')
|
|
firstChild.configLinkProperty('PlacementList')
|
|
|
|
if not hasProperty(firstChild,'ShowElement'):
|
|
firstChild.addProperty("App::PropertyBool","ShowElement",'','')
|
|
firstChild.setPropertyStatus('ShowElement',('Hidden','Immutable'))
|
|
firstChild.configLinkProperty('ShowElement')
|
|
|
|
if firstChild.AutoCount:
|
|
oldCount = getLinkProperty(info.Part[0],'ElementCount',None,True)
|
|
if oldCount is None:
|
|
firstChild.AutoCount = False
|
|
elif oldCount < count:
|
|
partTouched = 'Touched' in info.Part[0].State
|
|
setLinkProperty(info.Part[0],'ElementCount',count)
|
|
if not partTouched:
|
|
info.Part[0].purgeTouched()
|
|
|
|
if not firstChild.AutoCount:
|
|
oldCount = getLinkProperty(info.Part[0],'ElementCount')
|
|
if count > oldCount:
|
|
count = oldCount
|
|
|
|
if firstChild.Count != count:
|
|
firstChild.Count = count
|
|
firstChild.recompute()
|
|
|
|
if not touched and 'Touched' in firstChild.State:
|
|
# purge touched to avoid recomputation multi-pass
|
|
firstChild.purgeTouched()
|
|
|
|
# To solve the problem of element index reordering, we shall reorder the
|
|
# links array infos by its proximity to the corresponding constraining
|
|
# element shape
|
|
|
|
offset = FreeCAD.Vector(getattr(obj,'OffsetX',0),
|
|
getattr(obj,'OffsetY',0),
|
|
getattr(obj,'Offset',0))
|
|
poses = poses[:count]
|
|
infos0 = firstChild.Proxy.getInfo(expand=True)[:count]
|
|
|
|
used = [-1]*count
|
|
order = [None]*count
|
|
prev = getattr(self,'prevOrder',[])
|
|
distances = [10]*count
|
|
distMap = []
|
|
finished = 0
|
|
refPla = None
|
|
|
|
for i,info0 in enumerate(infos0):
|
|
pos0 = info0.Placement.multVec(
|
|
utils.getElementPos(info0.Shape)-offset)
|
|
if i<len(prev) and prev[i]<count:
|
|
j = prev[i]
|
|
if used[i]<0 and not order[j] and \
|
|
pos0.distanceToPoint(poses[j]) < 1e-7:
|
|
distances[i] = 0
|
|
if not elements[i]._refPla:
|
|
pla = infos[j].Placement.multiply(
|
|
utils.getElementPlacement(infos[j].Shape))
|
|
pla = pla.inverse().multiply(info.Placement)
|
|
elements[i]._refPla = pla
|
|
if not refPla:
|
|
refPla = pla
|
|
used[i] = j
|
|
order[j] = info0
|
|
finished += 1
|
|
continue
|
|
for j,pos in enumerate(poses):
|
|
if order[j]:
|
|
continue
|
|
d = pos0.distanceToPoint(pos)
|
|
if used[i]<0 and d < 1e-7:
|
|
distances[i] = 0
|
|
if not elements[i]._refPla:
|
|
pla = infos[j].Placement.multiply(
|
|
utils.getElementPlacement(infos[j].Shape))
|
|
pla = pla.inverse().multiply(info.Placement)
|
|
elements[i]._refPla = pla
|
|
if not refPla:
|
|
refPla = pla
|
|
used[i] = j
|
|
order[j] = info0
|
|
finished += 1
|
|
break
|
|
distMap.append((d,i,j))
|
|
|
|
count -= finished
|
|
if count:
|
|
distMap.sort()
|
|
# logger.debug('distance map: {}',len(distMap))
|
|
# for d in distMap:
|
|
# logger.debug(d)
|
|
for d,i,j in distMap:
|
|
if used[i]>=0 or order[j]:
|
|
continue
|
|
distances[i] = d
|
|
used[i] = j
|
|
order[j] = infos0[i]
|
|
count -= 1
|
|
if not count:
|
|
break
|
|
|
|
firstChild.Proxy.infos = order
|
|
self.prevOrder = used
|
|
|
|
from . import solver
|
|
if solver.isBusy():
|
|
return
|
|
|
|
# now for those instances that are 'out of place', lets assign some
|
|
# initial placement
|
|
|
|
partGroup = self.getAssembly().getPartGroup()
|
|
touched = False
|
|
for i,info0 in enumerate(infos0):
|
|
if not distances[i]:
|
|
continue
|
|
j = used[i]
|
|
info = infos[j]
|
|
|
|
# check if the instance is too far off the pairing element
|
|
p0 = utils.getElementPlacement(info0.Shape)
|
|
p0.Base -= offset
|
|
pla0 = info0.Placement.multiply(p0)
|
|
pla = info.Placement.multiply(
|
|
utils.getElementPlacement(info.Shape))
|
|
if distances[i]<=5 and \
|
|
abs(utils.getElementsAngle(pla.Rotation,pla0.Rotation))<45:
|
|
# if not too far off, just show it and let solver align it
|
|
showPart(partGroup,info0.Part)
|
|
continue
|
|
|
|
ref = elements[i]._refPla
|
|
if not ref:
|
|
ref = refPla
|
|
if ref:
|
|
pla = pla.multiply(ref)
|
|
else:
|
|
pla = pla.multiply(p0.inverse())
|
|
showPart(partGroup,info0.Part)
|
|
touched = True
|
|
|
|
# DO NOT purgeTouched here. We shall leave it as touched and
|
|
# trigger a second pass of recomputation to property update the
|
|
# associated element of this part.
|
|
#
|
|
# setPlacement(info0.Part,pla,purgeTouched=True)
|
|
#
|
|
setPlacement(info0.Part,pla)
|
|
|
|
# if touched:
|
|
# firstChild.Proxy.getInfo(True)
|
|
# firstChild.purgeTouched()
|
|
|
|
def execute(self,obj):
|
|
if not getattr(self,'_initializing',False) and\
|
|
getattr(self,'parent',None):
|
|
self.checkSupport()
|
|
if not self.version.update(self.childVersion()):
|
|
return
|
|
if Constraint.canMultiply(obj):
|
|
self.checkMultiply()
|
|
self.getElements(True)
|
|
Constraint.execute(obj)
|
|
self.version.commit()
|
|
return False
|
|
|
|
def getElements(self,refresh=False):
|
|
if refresh:
|
|
self.elements = None
|
|
obj = getattr(self,'Object',None)
|
|
if not obj:
|
|
return
|
|
ret = getattr(self,'elements',None)
|
|
if ret or Constraint.isDisabled(obj):
|
|
return ret
|
|
|
|
elementInfo = []
|
|
elements = []
|
|
group = flattenGroup(obj)
|
|
if Constraint.canMultiply(obj):
|
|
firstInfo = group[0].Proxy.getInfo(expand=True)
|
|
count = len(firstInfo)
|
|
if not count:
|
|
raise RuntimeError('invalid first element')
|
|
elements.append(group[0])
|
|
for o in group[1:]:
|
|
infos = o.Proxy.getInfo(expand=True)
|
|
if not infos:
|
|
continue
|
|
elements.append(o)
|
|
if count <= len(infos):
|
|
infos = infos[:count]
|
|
elementInfo += infos
|
|
break
|
|
elementInfo += infos
|
|
|
|
for info in zip(firstInfo,elementInfo):
|
|
Constraint.check(obj,info,True)
|
|
else:
|
|
for o in group:
|
|
checkType(o,AsmElementLink)
|
|
info = o.Proxy.getInfo()
|
|
if not info:
|
|
return
|
|
elementInfo.append(info)
|
|
elements.append(o)
|
|
Constraint.check(obj,elementInfo,True)
|
|
self.elements = elements
|
|
return self.elements
|
|
|
|
def isElementVisibleEx(self, _obj, _subname, _reason):
|
|
return 1
|
|
|
|
def getElementsInfo(self):
|
|
return [ e.Proxy.getInfo() for e in self.getElements() ]
|
|
|
|
Selection = namedtuple('AsmConstraintSelection',
|
|
('SelObject','SelSubname','Assembly','Constraint','Elements'))
|
|
|
|
@staticmethod
|
|
def getSelection(typeid=0,sels=None):
|
|
'''
|
|
Parse Gui.Selection for making a constraint
|
|
|
|
The selected elements must all belong to the same immediate parent
|
|
assembly.
|
|
'''
|
|
if not sels:
|
|
sels = FreeCADGui.Selection.getSelectionEx('',False)
|
|
if not sels:
|
|
raise RuntimeError('no selection')
|
|
if len(sels)>1:
|
|
raise RuntimeError(
|
|
'The selections must have a common (grand)parent assembly')
|
|
sel = sels[0]
|
|
subs = sel.SubElementNames
|
|
if not subs:
|
|
subs = ['']
|
|
|
|
cstr = None
|
|
elements = []
|
|
elementInfo = []
|
|
assembly = None
|
|
selSubname = None
|
|
infos = []
|
|
# first pass, collect hierarchy information, and find active assemble to
|
|
# use, i.e. which assembly to constraint
|
|
for sub in subs:
|
|
sobj = sel.Object.getSubObject(sub,1)
|
|
if not sobj:
|
|
raise RuntimeError('Cannot find sub-object {}.{}'.format(
|
|
sel.Object.Name,sub))
|
|
ret = Assembly.find(sel.Object,sub,
|
|
recursive=True,relativeToChild=False,keepEmptyChild=True)
|
|
if not ret:
|
|
raise RuntimeError('Selection {}.{} is not from an '
|
|
'assembly'.format(sel.Object.Name,sub))
|
|
|
|
infos.append((sub,sobj,ret))
|
|
|
|
if isTypeOf(sobj,Assembly,True):
|
|
assembly = ret[-1].Assembly
|
|
if sub:
|
|
selSubname = sub
|
|
elif isTypeOf(sobj,(AsmConstraintGroup,AsmConstraint)):
|
|
assembly = ret[-1].Assembly
|
|
selSubname = sub[:-len(ret[-1].Subname)]
|
|
elif not assembly:
|
|
assembly = ret[0].Assembly
|
|
selSubname = sub[:-len(ret[0].Subname)]
|
|
|
|
# second pass, collect element information
|
|
for sub,sobj,ret in infos:
|
|
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)))
|
|
|
|
if isTypeOf(sobj,Assembly,True) or \
|
|
isTypeOf(sobj,AsmConstraintGroup):
|
|
continue
|
|
|
|
if isTypeOf(sobj,AsmConstraint):
|
|
if cstr:
|
|
raise RuntimeError('more than one constraint selected')
|
|
cstr = sobj
|
|
continue
|
|
|
|
# because we call Assembly.find() above with relativeToChild=False,
|
|
# we shall adjust the element subname by popping the first '.'
|
|
sub = found.Subname
|
|
sub = sub[sub.index('.')+1:]
|
|
if sub[-1] == '.' and \
|
|
not isTypeOf(sobj,(AsmElement,AsmElementLink)):
|
|
# Too bad, its a full selection, let's guess the sub-element
|
|
if not utils.isElement((found.Object,sub)):
|
|
raise RuntimeError('no sub-element (face, edge, vertex) in '
|
|
'{}.{}'.format(found.Object.Name,sub))
|
|
subElement = utils.deduceSelectedElement(found.Object,sub)
|
|
if subElement:
|
|
sub += subElement
|
|
|
|
elements.append((found.Object,sub))
|
|
|
|
elementInfo.append(getElementInfo(
|
|
assembly,found.Object.Name+'.'+sub))
|
|
|
|
if not Constraint.isDisabled(cstr) and not Constraint.canMultiply(cstr):
|
|
if cstr:
|
|
typeid = Constraint.getTypeID(cstr)
|
|
check = []
|
|
for o in flattenGroup(cstr):
|
|
check.append(o.Proxy.getInfo())
|
|
elementInfo = check + elementInfo
|
|
|
|
Constraint.check(typeid,elementInfo)
|
|
|
|
return AsmConstraint.Selection(SelObject=sel.Object,
|
|
SelSubname=selSubname,
|
|
Assembly = assembly,
|
|
Constraint = cstr,
|
|
Elements = elements)
|
|
|
|
@staticmethod
|
|
def make(typeid,sel=None,name='Constraint',undo=True):
|
|
if not sel:
|
|
sel = AsmConstraint.getSelection(typeid)
|
|
assembly = resolveAssembly(sel.Assembly)
|
|
if sel.Constraint:
|
|
if undo:
|
|
FreeCAD.setActiveTransaction('Assembly change constraint')
|
|
cstr = sel.Constraint
|
|
else:
|
|
if undo:
|
|
FreeCAD.setActiveTransaction('Assembly create constraint')
|
|
constraints = assembly.getConstraintGroup()
|
|
cstr = constraints.Document.addObject("App::FeaturePython",
|
|
name,AsmConstraint(constraints),None,True)
|
|
ViewProviderAsmConstraint(cstr.ViewObject)
|
|
constraints.setLink({-1:cstr})
|
|
Constraint.setTypeID(cstr,typeid)
|
|
cstr.Label = Constraint.getTypeName(cstr)
|
|
|
|
try:
|
|
for e in sel.Elements:
|
|
AsmElementLink.make(AsmElementLink.MakeInfo(cstr,*e))
|
|
logger.catchDebug('init constraint', Constraint.init,cstr)
|
|
|
|
if gui.AsmCmdManager.AutoElementVis:
|
|
cstr.setPropertyStatus('VisibilityList','-Immutable')
|
|
cstr.VisibilityList = [False]*len(flattenGroup(cstr))
|
|
cstr.setPropertyStatus('VisibilityList','Immutable')
|
|
|
|
cstr.Proxy._initializing = False
|
|
|
|
if Constraint.canMultiply(cstr):
|
|
cstr.recompute(True)
|
|
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction()
|
|
undo = False
|
|
|
|
if sel.SelObject:
|
|
FreeCADGui.Selection.pushSelStack()
|
|
FreeCADGui.Selection.clearSelection()
|
|
if sel.SelSubname:
|
|
subname = sel.SelSubname
|
|
else:
|
|
subname = ''
|
|
subname += assembly.getConstraintGroup().Name + \
|
|
'.' + cstr.Name + '.'
|
|
FreeCADGui.Selection.addSelection(sel.SelObject,subname)
|
|
FreeCADGui.Selection.pushSelStack()
|
|
FreeCADGui.runCommand('Std_TreeSelection')
|
|
return cstr
|
|
|
|
except Exception as e:
|
|
logger.debug('failed to make constraint: {}',e)
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
|
|
@staticmethod
|
|
def makeMultiply(checkOnly=False):
|
|
sel = FreeCADGui.Selection.getSelectionEx('*',0)
|
|
if len(sel)!=1 or len(sel[0].SubElementNames)!=1:
|
|
raise RuntimeError('Too many selections')
|
|
|
|
sel = sel[0]
|
|
cstr = sel.Object.getSubObject(sel.SubElementNames[0],1)
|
|
if not isTypeOf(cstr,AsmConstraint):
|
|
raise RuntimeError('Must select a constraint')
|
|
|
|
multiplied = Constraint.canMultiply(cstr)
|
|
if multiplied is None:
|
|
raise RuntimeError('Constraint do not support multiplication')
|
|
|
|
elements = cstr.Proxy.getElements()
|
|
if len(elements)<2:
|
|
raise RuntimeError('Constraint must have more than one element')
|
|
|
|
if checkOnly:
|
|
return True
|
|
|
|
try:
|
|
FreeCAD.setActiveTransaction("Assembly constraint multiply")
|
|
|
|
info = elements[0].Proxy.getInfo()
|
|
if not isinstance(info.Part,tuple):
|
|
# The first element must be an link array in order to get
|
|
# multiplied.
|
|
|
|
#First, check if it is a link (with element count)
|
|
if getLinkProperty(info.Part,'ElementCount') is None:
|
|
# No. So we replace it with a link with command
|
|
# Std_LinkReplace, which requires a select of the object
|
|
# to be replaced first. So construct the selection path
|
|
# by replacing the last two subnames (i.e.
|
|
# Constraints.Constraint) with PartGroup.PartName
|
|
|
|
subs = flattenSubname(sel.Object,sel.SubElementNames[0])
|
|
subs = subs.split('.')
|
|
# The last entry is for sub-element name (e.g. Edge1,
|
|
# Face2), which should be empty
|
|
subs[-1] = ''
|
|
subs[-2] = info.Part.Name
|
|
subs[-3] = '2'
|
|
subs = '.'.join(subs)
|
|
# remember last selection
|
|
FreeCADGui.Selection.pushSelStack()
|
|
FreeCADGui.Selection.clearSelection()
|
|
FreeCADGui.Selection.addSelection(sel.Object,subs)
|
|
|
|
FreeCADGui.Selection.pushSelStack()
|
|
FreeCADGui.runCommand('Std_LinkReplace')
|
|
# restore the last selection
|
|
FreeCADGui.runCommand('Std_SelBack')
|
|
|
|
info = elements[0].Proxy.getInfo(True)
|
|
# make sure the replace command works
|
|
if getLinkProperty(info.Part,'ElementCount') is None:
|
|
raise RuntimeError('Failed to replace "{}" with a '
|
|
'link'.format(info.PartName))
|
|
|
|
# Let's first make an single element array without showing
|
|
# its element object, which will make the linked object
|
|
# grouped under the link rather than floating under tree
|
|
# view root
|
|
setLinkProperty(info.Part,'ShowElement',False)
|
|
try:
|
|
setLinkProperty(info.Part,'ElementCount',1)
|
|
except Exception:
|
|
raise RuntimeError('Failed to change element count of '
|
|
'{}'.format(info.PartName))
|
|
|
|
partGroup = cstr.Proxy.getAssembly().getPartGroup()
|
|
|
|
cstr.recompute(True)
|
|
|
|
if not multiplied:
|
|
for elementLink in elements[1:]:
|
|
subname = elementLink.Proxy.getElementSubname(True)
|
|
elementLink.Proxy.setLink(
|
|
partGroup,subname,checkOnly,multiply=True)
|
|
cstr.Multiply = True
|
|
else:
|
|
# Here means the constraint is already multiplied, expand it to
|
|
# multiple individual constraints
|
|
elements = cstr.Proxy.getElements()
|
|
infos0 = [(partGroup,'{}.{}.{}'.format(info.Part[0].Name,
|
|
info.Part[1],
|
|
info.Subname)) \
|
|
for info in elements[0].Proxy.getInfo(expand=True)]
|
|
infos = []
|
|
for element in elements[1:]:
|
|
if element.NoExpand:
|
|
infos.append(element.LinkedObject)
|
|
continue
|
|
info = element.Proxy.getInfo()
|
|
subs = Part.splitSubname(
|
|
element.Proxy.getElementSubname(True))
|
|
if isinstance(info.Part,tuple):
|
|
subs[0] = '{}.{}'.format(info.Part[1],subs[0])
|
|
parentShape = Part.getShape(
|
|
partGroup,subs[0],noElementMap=True)
|
|
subShape = parentShape.getElement(subs[2])
|
|
radius = utils.getElementCircular(subShape,True)
|
|
for i,edge in enumerate(parentShape.Edges):
|
|
if subShape.isCoplanar(edge) and \
|
|
utils.isSameValue(
|
|
utils.getElementCircular(edge,True),radius):
|
|
subs[2] = 'Edge{}'.format(i+1)
|
|
subs[1] = parentShape.getElementName(subs[2])
|
|
if subs[1] == subs[2]:
|
|
subs[1] = ''
|
|
infos.append((partGroup,Part.joinSubname(*subs)))
|
|
assembly = cstr.Proxy.getAssembly().Object
|
|
typeid = Constraint.getTypeID(cstr)
|
|
for info in zip(infos0,infos[:len(infos0)]):
|
|
sel = AsmConstraint.Selection(SelObject=None,
|
|
SelSubname=None,
|
|
Assembly = assembly,
|
|
Constraint = None,
|
|
Elements = info)
|
|
newCstr = AsmConstraint.make(typeid,sel,undo=False)
|
|
Constraint.copy(cstr,newCstr)
|
|
for element,target in zip(elements,newCstr.Group):
|
|
target.Offset = element.Offset
|
|
cstr.Document.removeObject(cstr.Name)
|
|
|
|
FreeCAD.closeActiveTransaction()
|
|
return True
|
|
except Exception:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
|
|
|
|
class ViewProviderAsmConstraint(ViewProviderAsmGroup):
|
|
|
|
def setupContextMenu(self,vobj,menu):
|
|
obj = vobj.Object
|
|
action = QtGui.QAction(QtGui.QIcon(),
|
|
"Enable constraint" if obj.Disabled else "Disable constraint", menu)
|
|
QtCore.QObject.connect(
|
|
action,QtCore.SIGNAL("triggered()"),self.toggleDisable)
|
|
menu.addAction(action)
|
|
|
|
def toggleDisable(self):
|
|
obj = self.ViewObject.Object
|
|
FreeCAD.setActiveTransaction('Toggle constraint')
|
|
try:
|
|
obj.Disabled = not obj.Disabled
|
|
FreeCAD.closeActiveTransaction()
|
|
except Exception:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
|
|
def attach(self,vobj):
|
|
super(ViewProviderAsmConstraint,self).attach(vobj)
|
|
vobj.OnTopWhenSelected = 2
|
|
try:
|
|
vobj.SwitchNode.overrideSwitch = 'OverrideVisible'
|
|
except Exception:
|
|
pass
|
|
|
|
def getIcon(self):
|
|
return Constraint.getIcon(self.ViewObject.Object)
|
|
|
|
def _getSelection(self,owner,subname,elements):
|
|
if not owner:
|
|
raise RuntimeError('no owner')
|
|
parent = getattr(owner.Proxy,'parent',None)
|
|
if isinstance(parent,AsmConstraintGroup):
|
|
# This can happen when we are dropping another element link from the
|
|
# same constraint group, in which case, 'owner' here will be the
|
|
# parent constraint of the dropping element link
|
|
subname = owner.Name + '.' + subname
|
|
owner = parent.Object
|
|
parent = parent.parent # ascend to the parent assembly
|
|
if not isinstance(parent,Assembly):
|
|
raise RuntimeError('not from the same assembly {},{}'.format(
|
|
objName(owner),parent))
|
|
subname = owner.Name + '.' + subname
|
|
obj = self.ViewObject.Object
|
|
mysub = parent.getConstraintGroup().Name + '.' + obj.Name + '.'
|
|
sel = []
|
|
if not elements:
|
|
elements = ['']
|
|
elements = [subname+element for element in elements]
|
|
elements.append(mysub)
|
|
sel = [Selection(Object=parent.Object,SubElementNames=elements)]
|
|
typeid = Constraint.getTypeID(obj)
|
|
return AsmConstraint.getSelection(typeid,sel)
|
|
|
|
def canDropObjectEx(self,_obj,owner,subname,elements):
|
|
cstr = self.ViewObject.Object
|
|
if logger.catchTrace('Cannot drop to AsmConstraint '
|
|
'{}'.format(cstr),self._getSelection,owner,subname,elements):
|
|
return True
|
|
return False
|
|
|
|
def dropObjectEx(self,_vobj,_obj,owner,subname,elements):
|
|
sel = self._getSelection(owner,subname,elements)
|
|
cstr = self.ViewObject.Object
|
|
typeid = Constraint.getTypeID(cstr)
|
|
sel = AsmConstraint.Selection(SelObject=None,
|
|
SelSubname=None,
|
|
Assembly=sel.Assembly,
|
|
Constraint=cstr,
|
|
Elements=sel.Elements)
|
|
AsmConstraint.make(typeid,sel,undo=False)
|
|
return '.'
|
|
|
|
def canDelete(self,_obj):
|
|
return True
|
|
|
|
|
|
class AsmConstraintGroup(AsmGroup):
|
|
def __init__(self,parent):
|
|
self.parent = getProxy(parent,Assembly)
|
|
super(AsmConstraintGroup,self).__init__()
|
|
|
|
def getAssembly(self):
|
|
return self.parent
|
|
|
|
def canLoadPartial(self,_obj):
|
|
return 2 if self.getAssembly().frozen else 0
|
|
|
|
def linkSetup(self,obj):
|
|
super(AsmConstraintGroup,self).linkSetup(obj)
|
|
if not hasProperty(obj,'_Version'):
|
|
obj.addProperty("App::PropertyInteger","_Version","Base",'')
|
|
obj.setPropertyStatus('_Version',['Hidden','Output'])
|
|
|
|
def onChanged(self,obj,prop):
|
|
if obj.Removing or FreeCAD.isRestoring():
|
|
return
|
|
if obj.Document and getattr(obj.Document,'Transacting',False):
|
|
return
|
|
if prop not in _IgnoredProperties:
|
|
Assembly.autoSolve(obj,prop)
|
|
|
|
@staticmethod
|
|
def make(parent,name='Constraints'):
|
|
obj = parent.Document.addObject("App::FeaturePython",name,
|
|
AsmConstraintGroup(parent),None,True)
|
|
ViewProviderAsmConstraintGroup(obj.ViewObject)
|
|
obj.purgeTouched()
|
|
return obj
|
|
|
|
|
|
class ViewProviderAsmConstraintGroup(ViewProviderAsmGroup):
|
|
_iconName = 'Assembly_Assembly_Constraints_Tree.svg'
|
|
|
|
def canDelete(self,_obj):
|
|
return True
|
|
|
|
def onDelete(self,_vobj,_subs):
|
|
return False
|
|
|
|
def updateData(self,obj,prop):
|
|
if prop == 'Group':
|
|
vis = len(obj.Group)!=0
|
|
vobj = obj.ViewObject
|
|
if vis != vobj.ShowInTree:
|
|
vobj.ShowInTree = vis
|
|
|
|
def canDropObjectEx(self,obj,_owner,_subname,_elements):
|
|
return AsmPlainGroup.contains(self.ViewObject.Object,obj)
|
|
|
|
def dropObjectEx(self,_vobj,obj,_owner,_subname,_elements):
|
|
AsmPlainGroup.tryMove(obj,self.ViewObject.Object)
|
|
|
|
def attach(self,vobj):
|
|
super(ViewProviderAsmConstraintGroup,self).attach(vobj)
|
|
try:
|
|
vobj.SwitchNode.overrideSwitch = 'OverrideReset'
|
|
except Exception:
|
|
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.cacheChildLabel()
|
|
# 'PartialTrigger' is just for silencing warning when partial load
|
|
self.Object.setPropertyStatus('VisibilityList', 'PartialTrigger')
|
|
|
|
def getAssembly(self):
|
|
return self.parent
|
|
|
|
def onChildLabelChange(self,obj,label):
|
|
names = set()
|
|
label = label.replace('.','_')
|
|
for o in flattenGroup(self.Object):
|
|
if o != obj:
|
|
names.add(o.Label)
|
|
if label not in names:
|
|
return label
|
|
for i,c in enumerate(reversed(label)):
|
|
if not c.isdigit():
|
|
if i:
|
|
label = label[:-i]
|
|
break;
|
|
i=0
|
|
while True:
|
|
i=i+1;
|
|
newLabel = '{}{:03d}'.format(label,i);
|
|
if newLabel!=obj.Label and newLabel not in names:
|
|
return newLabel
|
|
return label
|
|
|
|
@staticmethod
|
|
def make(parent,name='Elements'):
|
|
obj = parent.Document.addObject("App::FeaturePython",name,
|
|
AsmElementGroup(parent),None,True)
|
|
ViewProviderAsmElementGroup(obj.ViewObject)
|
|
obj.purgeTouched()
|
|
return obj
|
|
|
|
|
|
class ViewProviderAsmElementGroup(ViewProviderAsmGroup):
|
|
_iconName = 'Assembly_Assembly_Element_Tree.svg'
|
|
|
|
def setupContextMenu(self,_vobj,menu):
|
|
setupSortMenu(menu,self.sort,self.sortReverse)
|
|
|
|
def sortReverse(self):
|
|
sortChildren(self.ViewObject.Object,True)
|
|
|
|
def sort(self):
|
|
sortChildren(self.ViewObject.Object,False)
|
|
|
|
def canDropObjectEx(self,obj,owner,subname,elements):
|
|
if AsmPlainGroup.contains(self.ViewObject.Object,obj):
|
|
return True
|
|
if not owner:
|
|
return False
|
|
if not elements and not utils.isElement((owner,subname)):
|
|
return False
|
|
proxy = self.ViewObject.Object.Proxy
|
|
return proxy.getAssembly().getPartGroup()==owner
|
|
|
|
def dropObjectEx(self,vobj,obj,owner,subname,elements):
|
|
if AsmPlainGroup.tryMove(obj,self.ViewObject.Object):
|
|
return
|
|
|
|
sels = FreeCADGui.Selection.getSelectionEx('*',False)
|
|
if len(sels)==1 and \
|
|
len(sels[0].SubElementNames)==1 and \
|
|
sels[0].Object.getSubObject(
|
|
sels[0].SubElementNames[0],1)==vobj.Object:
|
|
sel = sels[0]
|
|
else:
|
|
sel = None
|
|
FreeCADGui.Selection.clearSelection()
|
|
res = self._drop(obj,owner,subname,elements)
|
|
if sel:
|
|
for element in res:
|
|
FreeCADGui.Selection.addSelection(sel.Object,
|
|
sel.SubElementNames[0]+element.Name+'.')
|
|
|
|
def _drop(self,obj,owner,subname,elements):
|
|
if not elements:
|
|
elements = ['']
|
|
res = []
|
|
for element in elements:
|
|
obj = AsmElement.make(AsmElement.Selection(
|
|
SelObj=None, SelSubname=None,
|
|
Element=None, Group=owner, Subname=subname+element))
|
|
if obj:
|
|
res.append(obj)
|
|
return res
|
|
|
|
def onDelete(self,_vobj,_subs):
|
|
return False
|
|
|
|
def canDelete(self,obj):
|
|
return isTypeOf(obj,AsmPlainGroup)
|
|
|
|
|
|
class AsmRelationGroup(AsmBase):
|
|
def __init__(self,parent):
|
|
self.relations = {}
|
|
self.parent = getProxy(parent,Assembly)
|
|
super(AsmRelationGroup,self).__init__()
|
|
|
|
def attach(self,obj):
|
|
# AsmRelationGroup do not install LinkBaseExtension
|
|
# obj.addExtension('App::LinkBaseExtensionPython', None)
|
|
|
|
obj.addProperty('App::PropertyLinkList','Group','')
|
|
obj.setPropertyStatus('Group','Hidden')
|
|
obj.addProperty('App::PropertyLink','Constraints','')
|
|
# this is to make sure relations are recomputed after all constraints
|
|
obj.Constraints = self.parent.getConstraintGroup()
|
|
obj.setPropertyStatus('Constraints',('Hidden','Immutable'))
|
|
self.linkSetup(obj)
|
|
|
|
def getViewProviderName(self,_obj):
|
|
return ''
|
|
|
|
def linkSetup(self,obj):
|
|
super(AsmRelationGroup,self).linkSetup(obj)
|
|
for o in obj.Group:
|
|
o.Proxy.parent = self
|
|
if o.Count:
|
|
for child in o.Group:
|
|
if isTypeOf(child,AsmRelation):
|
|
child.Proxy.parent = o.Proxy
|
|
|
|
def getAssembly(self):
|
|
return self.parent
|
|
|
|
def hasChildElement(self,_obj):
|
|
return True
|
|
|
|
def isElementVisible(self,obj,element):
|
|
child = obj.Document.getObject(element)
|
|
if not child or not getattr(child,'Part',None):
|
|
return 0
|
|
return self.parent.getPartGroup().isElementVisible(child.Part.Name)
|
|
|
|
def setElementVisible(self,obj,element,vis):
|
|
child = obj.Document.getObject(element)
|
|
if not child or not getattr(child,'Part',None):
|
|
return 0
|
|
return self.parent.getPartGroup().setElementVisible(child.Part.Name,vis)
|
|
|
|
def canLoadPartial(self,_obj):
|
|
return 2 if self.getAssembly().frozen else 0
|
|
|
|
def getRelations(self,refresh=False):
|
|
if not refresh and getattr(self,'relations',None):
|
|
return self.relations
|
|
obj = self.Object
|
|
self.relations = {}
|
|
for o in obj.Group:
|
|
if o.Part:
|
|
self.relations[o.Part] = o
|
|
group = []
|
|
relations = self.relations.copy()
|
|
touched = False
|
|
new = []
|
|
for part in self.getAssembly().getPartGroup().LinkedChildren:
|
|
o = relations.get(part,None)
|
|
if not o:
|
|
touched = True
|
|
new.append(AsmRelation.make(obj,part))
|
|
group.append(new[-1])
|
|
self.relations[part] = new[-1]
|
|
else:
|
|
group.append(o)
|
|
relations.pop(part)
|
|
|
|
if relations or touched:
|
|
obj.Group = group
|
|
obj.purgeTouched()
|
|
|
|
removes = []
|
|
for k,o in relations.items():
|
|
self.relations.pop(k)
|
|
if o.Count:
|
|
for child in o.Group:
|
|
if isTypeOf(child,AsmRelation):
|
|
removes.append(child.Name)
|
|
try:
|
|
# This could fail if the object is already deleted due to
|
|
# undo/redo
|
|
removes.append(o.Name)
|
|
except Exception:
|
|
pass
|
|
|
|
Assembly.scheduleDelete(obj.Document,removes)
|
|
|
|
for o in new:
|
|
o.Proxy.getConstraints()
|
|
|
|
return self.relations
|
|
|
|
def findRelation(self,part):
|
|
relations = self.getRelations()
|
|
if not isinstance(part,tuple):
|
|
return relations.get(part,None)
|
|
|
|
relation = relations.get(part[0],None)
|
|
if not relation:
|
|
return
|
|
if part[1]>=relation.Count:
|
|
relation.recompute()
|
|
group = relation.Group
|
|
try:
|
|
relation = group[part[1]]
|
|
checkType(relation,AsmRelation)
|
|
return relation
|
|
except Exception as e:
|
|
logger.error('invalid relation of part array: {}',e)
|
|
|
|
def update(self,cstr,oldPart,newPart,partName):
|
|
relation = self.findRelation(oldPart)
|
|
if relation:
|
|
try:
|
|
group = relation.Group
|
|
group.remove(cstr)
|
|
relation.Group = group
|
|
except ValueError:
|
|
pass
|
|
relation = self.findRelation(newPart)
|
|
if not relation:
|
|
logger.warn('Cannot find relation of part {}',partName)
|
|
elif cstr not in relation.Group:
|
|
relation.Group = {-1:cstr}
|
|
|
|
@staticmethod
|
|
def make(parent,name='Relations'):
|
|
obj = parent.Document.addObject("App::FeaturePython",name,
|
|
AsmRelationGroup(parent),None,True)
|
|
ViewProviderAsmRelationGroup(obj.ViewObject)
|
|
obj.Label = name
|
|
obj.purgeTouched()
|
|
return obj
|
|
|
|
@staticmethod
|
|
def gotoRelationOfConstraint(obj,subname):
|
|
sobj = obj.getSubObject(subname,1)
|
|
if not isTypeOf(sobj,AsmConstraint):
|
|
return
|
|
subname = flattenLastSubname(obj,subname)
|
|
sub = Part.splitSubname(subname)[0].split('.')
|
|
sub = sub[:-1]
|
|
sub[-2] = '3'
|
|
sub[-1] = ''
|
|
sub = '.'.join(sub)
|
|
subs = []
|
|
relationGroup = sobj.Proxy.getAssembly().getRelationGroup(True)
|
|
for relation in relationGroup.Proxy.getRelations().values():
|
|
for o in relation.Group:
|
|
if isTypeOf(o,AsmRelation):
|
|
found = False
|
|
for child in o.Group:
|
|
if child == sobj:
|
|
subs.append('{}{}.{}.{}.'.format(
|
|
sub,relation.Name,o.Name,child.Name))
|
|
found = True
|
|
break
|
|
if found:
|
|
continue
|
|
elif o == sobj:
|
|
subs.append('{}{}.{}.'.format(sub,relation.Name,o.Name))
|
|
|
|
if subs:
|
|
FreeCADGui.Selection.pushSelStack()
|
|
FreeCADGui.Selection.clearSelection()
|
|
FreeCADGui.Selection.addSelection(obj,subs)
|
|
FreeCADGui.Selection.pushSelStack()
|
|
FreeCADGui.runCommand('Std_TreeSelection')
|
|
|
|
@staticmethod
|
|
def gotoRelation(moveInfo):
|
|
if not moveInfo:
|
|
return
|
|
|
|
subname = moveInfo.SelSubname
|
|
info = moveInfo.ElementInfo
|
|
sobj = moveInfo.SelObj.getSubObject(moveInfo.SelSubname,1)
|
|
|
|
if isTypeOf(sobj,AsmConstraint):
|
|
AsmRelationGroup.gotoRelationOfConstraint(
|
|
moveInfo.SelObj, moveInfo.SelSubname)
|
|
return
|
|
|
|
if len(moveInfo.HierarchyList)>1 and \
|
|
isTypeOf(sobj,(AsmElement,AsmElementLink)):
|
|
hierarchy = moveInfo.HierarchyList[-1]
|
|
info = getElementInfo(hierarchy.Object, hierarchy.Subname)
|
|
else:
|
|
hierarchy = moveInfo.Hierarchy
|
|
|
|
if not info.Subname:
|
|
subname = flattenLastSubname(moveInfo.SelObj,subname,hierarchy)
|
|
subs = subname.split('.')
|
|
elif moveInfo.SelSubname.endswith(info.Subname):
|
|
subname = flattenLastSubname(
|
|
moveInfo.SelObj,subname[:-len(info.Subname)])
|
|
subs = subname.split('.')
|
|
else:
|
|
subname = flattenLastSubname(moveInfo.SelObj,subname,hierarchy)
|
|
subs = subname.split('.')
|
|
if isTypeOf(sobj,AsmElementLink):
|
|
subs = subs[:-3]
|
|
elif isTypeOf(sobj,AsmElement):
|
|
subs = subs[:-2]
|
|
else:
|
|
raise RuntimeError('Invalid selection {}.{}, {}'.format(
|
|
objName(moveInfo.SelObj),moveInfo.SelSubname,subname))
|
|
if isinstance(info.Part,tuple):
|
|
subs += ['','','']
|
|
else:
|
|
subs += ['','']
|
|
|
|
relationGroup = resolveAssembly(info.Parent).getRelationGroup(True)
|
|
if isinstance(info.Part,tuple):
|
|
part = info.Part[0]
|
|
else:
|
|
part = info.Part
|
|
relation = relationGroup.Proxy.findRelation(part)
|
|
if not relation:
|
|
return
|
|
if isinstance(info.Part,tuple):
|
|
if len(subs)<4:
|
|
subs.append('')
|
|
subs[-4] = '3'
|
|
subs[-3] = relation.Name
|
|
subs[-2] = relation.Group[info.Part[1]].Name
|
|
else:
|
|
subs[-3] = '3'
|
|
subs[-2] = relation.Name
|
|
FreeCADGui.Selection.pushSelStack()
|
|
FreeCADGui.Selection.clearSelection()
|
|
FreeCADGui.Selection.addSelection(moveInfo.SelObj,'.'.join(subs))
|
|
FreeCADGui.Selection.pushSelStack()
|
|
FreeCADGui.runCommand('Std_TreeSelection')
|
|
|
|
|
|
class ViewProviderAsmRelationGroup(ViewProviderAsmBase):
|
|
_iconName = 'Assembly_Assembly_Relation_Tree.svg'
|
|
|
|
def canDropObjects(self):
|
|
return False
|
|
|
|
def claimChildren(self):
|
|
return self.ViewObject.Object.Group
|
|
|
|
def onDelete(self,vobj,_subs):
|
|
obj = vobj.Object
|
|
relations = obj.Group
|
|
obj.Group = []
|
|
for o in relations:
|
|
if o.Count:
|
|
group = o.Group
|
|
o.Group = []
|
|
for child in group:
|
|
if isTypeOf(child,AsmRelation):
|
|
child.Document.removeObject(child.Name)
|
|
o.Document.removeObject(o.Name)
|
|
return True
|
|
|
|
|
|
class AsmRelation(AsmBase):
|
|
def __init__(self,parent):
|
|
self.parent = getProxy(parent,(AsmRelationGroup,AsmRelation))
|
|
super(AsmRelation,self).__init__()
|
|
|
|
def linkSetup(self,obj):
|
|
super(AsmRelation,self).linkSetup(obj)
|
|
obj.configLinkProperty(LinkedObject = 'Part')
|
|
|
|
def attach(self,obj):
|
|
obj.addProperty("App::PropertyLink","Part"," Link",'')
|
|
obj.setPropertyStatus('Part','ReadOnly')
|
|
obj.addProperty("App::PropertyInteger","Count"," Link",'')
|
|
obj.setPropertyStatus('Count','Hidden')
|
|
obj.addProperty("App::PropertyInteger","Index"," Link",'')
|
|
obj.setPropertyStatus('Index','Hidden')
|
|
obj.addProperty('App::PropertyLinkList','Group','')
|
|
obj.setPropertyStatus('Group','Hidden')
|
|
super(AsmRelation,self).attach(obj)
|
|
|
|
def getSubObject(self,obj,subname,retType,mat,transform,depth):
|
|
if not subname or subname[0]==';':
|
|
return False
|
|
idx = subname.find('.')
|
|
if idx<0:
|
|
return False
|
|
name = subname[:idx]
|
|
for o in obj.Group:
|
|
if o.Name == name:
|
|
return o.getSubObject(subname[idx+1:],
|
|
retType,mat,transform,depth+1)
|
|
|
|
def getAssembly(self):
|
|
return self.parent.getAssembly()
|
|
|
|
def updateLabel(self):
|
|
obj = self.Object
|
|
if obj.Part:
|
|
obj.Label = obj.Part.Label
|
|
|
|
def execute(self,obj):
|
|
part = obj.Part
|
|
if not part:
|
|
return False
|
|
|
|
if not isinstance(self.parent,AsmRelationGroup):
|
|
return False
|
|
|
|
count = getLinkProperty(part,'ElementCount',0)
|
|
remove = []
|
|
if obj.Count > count:
|
|
group = obj.Group
|
|
remove = [o.Name for o in group[count:]]
|
|
obj.Group = group[:count]
|
|
Assembly.scheduleDelete(obj.Document,remove)
|
|
obj.Count = count
|
|
self.getConstraints()
|
|
elif obj.Count < count:
|
|
new = []
|
|
for i in range(obj.Count,count):
|
|
new.append(AsmRelation.make(obj,(part,i)))
|
|
obj.Count = count
|
|
obj.Group = obj.Group[:obj.Count]+new
|
|
for o in new:
|
|
o.Proxy.getConstraints()
|
|
|
|
return False
|
|
|
|
def allowDuplicateLabel(self,_obj):
|
|
return True
|
|
|
|
def hasChildElement(self,_obj):
|
|
return True
|
|
|
|
def _getGroup(self):
|
|
if isinstance(self.parent,AsmRelation):
|
|
return self.parent.Object.Part
|
|
return self.getAssembly().getConstraintGroup()
|
|
|
|
def isElementVisible(self,obj,element):
|
|
if not obj.Part:
|
|
return
|
|
child = obj.Document.getObject(element)
|
|
if isTypeOf(child,AsmRelation):
|
|
group = obj.Part
|
|
element = str(child.Index)
|
|
else:
|
|
group = self.getAssembly().getConstraintGroup()
|
|
return group.isElementVisible(element)
|
|
|
|
def setElementVisible(self,obj,element,vis):
|
|
if not obj.Part:
|
|
return
|
|
child = obj.Document.getObject(element)
|
|
if isTypeOf(child,AsmRelation):
|
|
group = obj.Part
|
|
element = str(child.Index)
|
|
else:
|
|
group = self.getAssembly().getConstraintGroup()
|
|
return group.setElementVisible(element,vis)
|
|
|
|
def redirectSubName(self,obj,subname,_topParent,child):
|
|
if not obj.Part:
|
|
return
|
|
if isinstance(self.parent,AsmRelation):
|
|
subname = subname.split('.')
|
|
if not child:
|
|
subname[-3] = self.getAssembly().getPartGroup().Name
|
|
subname[-2] = obj.Part.Name
|
|
subname[-1] = str(obj.Index)
|
|
subname.append('')
|
|
else:
|
|
subname[-3] = self.getAssembly().getConstraintGroup().Name
|
|
subname[-2] = ''
|
|
subname = subname[:-1]
|
|
elif not child:
|
|
subname = subname.split('.')
|
|
subname[-2] = self.getAssembly().getPartGroup().Name
|
|
subname[-1] = obj.Part.Name
|
|
subname.append('')
|
|
elif isTypeOf(child,AsmConstraint):
|
|
subname = subname.split('.')
|
|
subname[-2] = self.getAssembly().getConstraintGroup().Name
|
|
else:
|
|
return
|
|
return '.'.join(subname)
|
|
|
|
def getConstraints(self):
|
|
obj = self.Object
|
|
if obj.Count or not obj.Part:
|
|
return
|
|
if isinstance(self.parent,AsmRelation):
|
|
part = (obj.Part,obj.Index)
|
|
else:
|
|
part = obj.Part
|
|
group = []
|
|
for cstr in flattenGroup(self.getAssembly().getConstraintGroup()):
|
|
for element in cstr.Group:
|
|
info = element.Proxy.getInfo()
|
|
if isinstance(info.Part,tuple):
|
|
infoPart = info.Part[:2]
|
|
else:
|
|
infoPart = info.Part
|
|
if infoPart==part:
|
|
group.append(cstr)
|
|
break
|
|
obj.Group = group
|
|
obj.purgeTouched()
|
|
|
|
@staticmethod
|
|
def make(parent,part,name='Relation'):
|
|
obj = parent.Document.addObject("App::FeaturePython",name,
|
|
AsmRelation(parent),None,True)
|
|
ViewProviderAsmRelation(obj.ViewObject)
|
|
if isinstance(part,tuple):
|
|
obj.setLink(part[0])
|
|
obj.Index = part[1]
|
|
obj.Label = str(part[1])
|
|
else:
|
|
obj.setLink(part)
|
|
obj.Label = part.Label
|
|
obj.recompute()
|
|
obj.setPropertyStatus('Index','Immutable')
|
|
obj.purgeTouched()
|
|
return obj
|
|
|
|
|
|
class ViewProviderAsmRelation(ViewProviderAsmBase):
|
|
|
|
def canDropObjects(self):
|
|
return False
|
|
|
|
def onDelete(self,_vobj,_subs):
|
|
return False
|
|
|
|
def canDelete(self,_obj):
|
|
return True
|
|
|
|
def claimChildren(self):
|
|
return self.ViewObject.Object.Group
|
|
|
|
|
|
BuildShapeNone = 'None'
|
|
BuildShapeCompound = 'Compound'
|
|
BuildShapeFuse = 'Fuse'
|
|
BuildShapeCut = 'Cut'
|
|
BuildShapeCommon = 'Common'
|
|
BuildShapeNames = (BuildShapeNone,BuildShapeCompound,
|
|
BuildShapeFuse,BuildShapeCut,BuildShapeCommon)
|
|
|
|
class Assembly(AsmGroup):
|
|
_Busy = False
|
|
_PartMap = {} # maps part to assembly
|
|
_PartArrayMap = {} # maps array part to assembly
|
|
_ScheduleTimer = QtCore.QTimer()
|
|
_PendingReload = defaultdict(set)
|
|
_PendingSolve = False
|
|
|
|
def __init__(self):
|
|
self.parts = set()
|
|
self.partArrays = set()
|
|
self.constraints = None
|
|
self.frozen = False
|
|
self.deleting = False
|
|
super(Assembly,self).__init__()
|
|
|
|
def getSubObjects(self,obj,reason):
|
|
# Deletion order problem may cause exception here. Just silence it
|
|
try:
|
|
if reason:
|
|
return [o.Name+'.' for o in obj.Group]
|
|
partGroup = self.getPartGroup()
|
|
return ['{}.{}'.format(partGroup.Name,name)
|
|
for name in partGroup.getSubObjects(reason)]
|
|
except Exception:
|
|
pass
|
|
|
|
def _collectParts(self,oldParts,newParts,partMap):
|
|
for part in newParts:
|
|
try:
|
|
oldParts.remove(part)
|
|
except KeyError:
|
|
partMap[part] = self
|
|
for part in oldParts:
|
|
del partMap[part]
|
|
|
|
def execute(self,obj):
|
|
if self.frozen:
|
|
return True
|
|
|
|
parts = set()
|
|
partArrays = set()
|
|
self.constraints = None
|
|
|
|
self.buildShape()
|
|
System.touch(obj)
|
|
obj.ViewObject.Proxy.onExecute()
|
|
|
|
# collect the part objects of this assembly
|
|
for cstr in self.getConstraints():
|
|
for element in cstr.Proxy.getElements():
|
|
info = element.Proxy.getInfo()
|
|
if isinstance(info.Part,tuple):
|
|
partArrays.add(info.Part[0])
|
|
parts.add(info.Part[0])
|
|
else:
|
|
parts.add(info.Part)
|
|
|
|
# Update the global part object list for auto solving
|
|
#
|
|
# Assembly._PartMap is used to track normal part object for change in
|
|
# its 'Placement'
|
|
#
|
|
# Assembly._PartArrayMap is for tracking link array for change in its
|
|
# 'PlacementList'
|
|
|
|
self._collectParts(self.parts,parts,Assembly._PartMap)
|
|
self.parts = parts
|
|
self._collectParts(self.partArrays,partArrays,Assembly._PartArrayMap)
|
|
self.partArrays = partArrays
|
|
|
|
return False # return False to call LinkBaseExtension::execute()
|
|
|
|
@classmethod
|
|
def canAutoSolve(cls):
|
|
from . import solver
|
|
# return gui.AsmCmdManager.WorkbenchActivated and \
|
|
return gui.AsmCmdManager.AutoRecompute and \
|
|
FreeCADGui.ActiveDocument and \
|
|
not FreeCADGui.ActiveDocument.Transacting and \
|
|
not FreeCAD.isRestoring() and \
|
|
not solver.isBusy() and \
|
|
not ViewProviderAssembly.isBusy()
|
|
|
|
@classmethod
|
|
def checkPartChange(cls, obj, prop):
|
|
if prop == 'Label':
|
|
try:
|
|
cls._PartMap.get(obj).getRelationGroup().\
|
|
Proxy.findRelation(obj).\
|
|
Proxy.updateLabel()
|
|
except Exception:
|
|
pass
|
|
return
|
|
|
|
if not cls.canAutoSolve() or prop in _IgnoredProperties:
|
|
return
|
|
assembly = None
|
|
if prop == 'Placement':
|
|
partMap = cls._PartMap
|
|
assembly = partMap.get(obj,None)
|
|
elif prop == 'PlacementList':
|
|
partMap = cls._PartArrayMap
|
|
assembly = partMap.get(obj,None)
|
|
if assembly:
|
|
try:
|
|
# This will fail if assembly got deleted
|
|
assembly.Object.Name
|
|
except Exception:
|
|
del partMap[obj]
|
|
else:
|
|
cls.autoSolve(obj,prop,True)
|
|
|
|
@classmethod
|
|
def autoSolve(cls,obj,prop,force=False):
|
|
if obj.Document and getattr(obj.Document,'Transacting',False):
|
|
cls.cancelAutoSolve()
|
|
return
|
|
if not force and cls._PendingSolve:
|
|
return
|
|
if force or cls.canAutoSolve():
|
|
logger.debug('auto solve scheduled on change of {}.{}',
|
|
objName(obj),prop,frame=1)
|
|
cls._PendingSolve = True
|
|
|
|
@classmethod
|
|
def cancelAutoSolve(cls):
|
|
logger.debug('cancel auto solve',frame=1)
|
|
cls._PendingSolve = False
|
|
|
|
@classmethod
|
|
def doAutoSolve(cls):
|
|
if not cls._PendingSolve:
|
|
return
|
|
canSolve = cls.canAutoSolve()
|
|
if cls._Busy or not canSolve:
|
|
cls._PendingSolve = canSolve
|
|
return
|
|
|
|
cls.cancelAutoSolve()
|
|
|
|
from . import solver
|
|
logger.debug('start solving...')
|
|
logger.catch('solver exception when auto recompute',
|
|
solver.solve, FreeCAD.ActiveDocument.Objects, True)
|
|
logger.debug('done solving')
|
|
|
|
@classmethod
|
|
def scheduleDelete(cls,doc,names):
|
|
# FC core now support pending remove, so no need to schedule here
|
|
for name in names:
|
|
try:
|
|
doc.removeObject(name)
|
|
except Exception:
|
|
pass
|
|
|
|
@classmethod
|
|
def scheduleReload(cls,obj):
|
|
cls._PendingReload[obj.Document.Name].add(obj.Name)
|
|
cls.schedule()
|
|
|
|
@classmethod
|
|
def schedule(cls):
|
|
if not cls._ScheduleTimer.isSingleShot():
|
|
cls._ScheduleTimer.setSingleShot(True)
|
|
cls._ScheduleTimer.timeout.connect(Assembly.onSchedule)
|
|
if not cls._ScheduleTimer.isActive():
|
|
cls._ScheduleTimer.start(50)
|
|
|
|
@classmethod
|
|
def pauseSchedule(cls):
|
|
cls._Busy = True
|
|
cls._ScheduleTimer.stop()
|
|
|
|
@classmethod
|
|
def resumeSchedule(cls):
|
|
cls._Busy = False
|
|
cls.schedule()
|
|
|
|
@classmethod
|
|
def onSchedule(cls):
|
|
for name,onames in cls._PendingReload.items():
|
|
doc = FreeCADGui.reload(name)
|
|
if not doc:
|
|
break
|
|
for oname in onames:
|
|
obj = doc.getObject(oname)
|
|
if getattr(obj,'Freeze',None):
|
|
obj.Freeze = False
|
|
cls._PendingReload.clear()
|
|
|
|
def onSolverChanged(self):
|
|
for obj in self.getConstraintGroup().LinkedChildren:
|
|
# setup==True usually means we are restoring, so try to restore the
|
|
# non-touched state if possible, since recompute() below will touch
|
|
# the constraint object
|
|
touched = 'Touched' in obj.State
|
|
obj.recompute()
|
|
if not touched:
|
|
obj.purgeTouched()
|
|
|
|
def upgrade(self):
|
|
'Upgrade old assembly objects to the new version'
|
|
partGroup = self.getPartGroup()
|
|
if partGroup.isDerivedFrom('Part::FeaturePython'):
|
|
return
|
|
partGroup.setPropertyStatus('GroupMode','-Immutable')
|
|
partGroup.GroupMode = 0 # prevent auto delete children
|
|
newPartGroup = AsmPartGroup.make(self.Object)
|
|
newPartGroup.Group = partGroup.Group
|
|
newPartGroup.setPropertyStatus('VisibilityList','-Immutable')
|
|
newPartGroup.VisibilityList = partGroup.VisibilityList
|
|
newPartGroup.setPropertyStatus('VisibilityList','Immutable')
|
|
|
|
elementGroup = self.getElementGroup()
|
|
vis = elementGroup.VisibilityList
|
|
elements = []
|
|
old = elementGroup.Group
|
|
for element in old:
|
|
copy = AsmElement.create('Element',elementGroup)
|
|
link = element.LinkedObject
|
|
if isinstance(link,tuple):
|
|
copy.LinkedObject = (newPartGroup,link[1])
|
|
copy.Label = element.Label
|
|
copy.Proxy._initializing = False
|
|
elements.append(copy)
|
|
|
|
elementGroup.setPropertyStatus('Group','-Immutable')
|
|
elementGroup.Group = elements
|
|
elementGroup.setPropertyStatus('Group','Immutable')
|
|
elementGroup.setPropertyStatus('VisibilityList','-Immutable')
|
|
elementGroup.VisibilityList = vis
|
|
elementGroup.setPropertyStatus('VisibilityList','Immutable')
|
|
elementGroup.cacheChildLabel()
|
|
|
|
for element in old:
|
|
element.Document.removeObject(element.Name)
|
|
|
|
self.Object.setLink({2:newPartGroup})
|
|
|
|
# no need to remove the object as Assembly has group mode of AutoDelete
|
|
#
|
|
# partGroup.Document.removeObject(partGroup.Name)
|
|
|
|
elementGroup.recompute(True)
|
|
|
|
def buildShape(self):
|
|
obj = self.Object
|
|
partGroup = self.getPartGroup()
|
|
if not obj.Freeze and obj.BuildShape==BuildShapeNone:
|
|
obj.Shape = Part.Shape();
|
|
try:
|
|
partGroup.Shape = Part.Shape()
|
|
except Exception:
|
|
pass
|
|
return
|
|
|
|
group = flattenGroup(partGroup)
|
|
|
|
shapes = []
|
|
if obj.BuildShape == BuildShapeCompound or \
|
|
(obj.BuildShape==BuildShapeNone and obj.Freeze):
|
|
for o in group:
|
|
if partGroup.isElementVisible(o.Name):
|
|
shape = Part.getShape(o)
|
|
if not shape.isNull():
|
|
shapes.append(shape)
|
|
else:
|
|
# first shape is always included regardless of its visibility
|
|
solids = Part.getShape(group[0]).Solids
|
|
if solids:
|
|
if len(solids)>1 and obj.BuildShape!=BuildShapeFuse:
|
|
shapes.append(solids[0].fuse(solids[1:]))
|
|
else:
|
|
shapes += solids
|
|
group = group[1:]
|
|
for o in group:
|
|
if partGroup.isElementVisible(o.Name):
|
|
shape = Part.getShape(o)
|
|
# in case the first part have solids, we only include
|
|
# subsequent part containing solid
|
|
if solids:
|
|
shapes += shape.Solids
|
|
else:
|
|
shapes += shape
|
|
if not shapes:
|
|
raise RuntimeError('No shape found in parts')
|
|
if len(shapes) == 1:
|
|
# hide shape placement, and get element mapping
|
|
shape = Part.makeCompound(shapes)
|
|
elif obj.BuildShape == BuildShapeFuse:
|
|
shape = shapes[0].fuse(shapes[1:])
|
|
elif obj.BuildShape == BuildShapeCut:
|
|
shape = shapes[0].cut(shapes[1:])
|
|
elif obj.BuildShape == BuildShapeCommon:
|
|
shape = shapes[0].common(shapes[1:])
|
|
else:
|
|
shape = Part.makeCompound(shapes)
|
|
|
|
try:
|
|
if obj.Freeze or obj.BuildShape!=BuildShapeCompound:
|
|
partGroup.Shape = shape
|
|
shape.Tag = partGroup.ID
|
|
else:
|
|
partGroup.Shape = Part.Shape()
|
|
except Exception:
|
|
pass
|
|
|
|
shape.Placement = obj.Placement
|
|
obj.Shape = shape
|
|
|
|
def attach(self, obj):
|
|
obj.addProperty("App::PropertyEnumeration","BuildShape","Base",'')
|
|
obj.addProperty("App::PropertyInteger","_Version","Base",'')
|
|
obj.setPropertyStatus('_Version',['Hidden','Output'])
|
|
obj._Version = 1
|
|
obj.BuildShape = BuildShapeNames
|
|
super(Assembly,self).attach(obj)
|
|
|
|
def linkSetup(self,obj):
|
|
self.parts = set()
|
|
self.partArrays = set()
|
|
obj.configLinkProperty('Placement')
|
|
if not hasProperty(obj,'ColoredElements'):
|
|
obj.addProperty("App::PropertyLinkSubHidden",
|
|
"ColoredElements","Base",'')
|
|
obj.setPropertyStatus('ColoredElements',('Hidden','Immutable'))
|
|
obj.configLinkProperty('ColoredElements')
|
|
if not hasProperty(obj,'Freeze'):
|
|
obj.addProperty('App::PropertyBool','Freeze','Base','')
|
|
obj.setPropertyStatus('Freeze','PartialTrigger')
|
|
super(Assembly,self).linkSetup(obj)
|
|
obj.setPropertyStatus('Group','-Output')
|
|
System.attach(obj)
|
|
|
|
# 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
|
|
partGroup = self.getPartGroup(True)
|
|
|
|
if not getattr(obj,'_Version',None):
|
|
cstrGroup = self.getConstraintGroup().Proxy
|
|
for o in flattenGroup(cstrGroup.Object):
|
|
cstr = getProxy(o,AsmConstraint)
|
|
cstr.parent = cstrGroup
|
|
for oo in flattenGroup(o):
|
|
oo.Proxy.parent = cstr
|
|
elementGroup = self.getElementGroup().Proxy
|
|
for o in flattenGroup(elementGroup.Object):
|
|
element = getProxy(o,AsmElement)
|
|
element.parent = elementGroup
|
|
|
|
self.getRelationGroup()
|
|
|
|
self.frozen = obj.Freeze
|
|
if not self.frozen:
|
|
cstrGroup = self.getConstraintGroup()
|
|
if cstrGroup._Version<=0:
|
|
cstrGroup._Version = 1
|
|
for cstr in flattenGroup(cstrGroup):
|
|
for link in flattenGroup(cstr):
|
|
link.Proxy.migrate(link)
|
|
|
|
if self.frozen or partGroup.isDerivedFrom('Part::FeaturePython'):
|
|
shape = Part.Shape(partGroup.Shape)
|
|
shape.Placement = obj.Placement
|
|
shape.Tag = obj.ID
|
|
obj.Shape = shape
|
|
if obj.Shape.isNull() and \
|
|
obj.BuildShape == BuildShapeCompound:
|
|
self.buildShape()
|
|
|
|
System.touch(obj,False)
|
|
|
|
def onChanged(self, obj, prop):
|
|
if obj.Removing or \
|
|
not getattr(self,'Object',None) or \
|
|
FreeCAD.isRestoring():
|
|
return
|
|
if obj.Document and getattr(obj.Document,'Transacting',False):
|
|
if prop == 'Freeze':
|
|
self.frozen = obj.Freeze
|
|
System.onChanged(obj,prop)
|
|
return
|
|
if prop == 'BuildShape':
|
|
self.buildShape()
|
|
return
|
|
if prop == 'Freeze':
|
|
if obj.Freeze == self.frozen:
|
|
return
|
|
if obj.Document.Partial:
|
|
Assembly.scheduleReload(obj)
|
|
return
|
|
self.upgrade()
|
|
if obj.BuildShape==BuildShapeNone:
|
|
self.buildShape()
|
|
elif obj.Freeze:
|
|
self.getPartGroup().Shape = obj.Shape
|
|
else:
|
|
self.getPartGroup().Shape = Part.Shape()
|
|
self.frozen = obj.Freeze
|
|
return
|
|
if prop!='Group' and prop not in _IgnoredProperties:
|
|
System.onChanged(obj,prop)
|
|
Assembly.autoSolve(obj,prop)
|
|
|
|
def getConstraintGroup(self, create=False):
|
|
obj = self.Object
|
|
try:
|
|
ret = obj.Group[0]
|
|
if obj.Freeze:
|
|
if not isTypeOf(ret,AsmConstraintGroup):
|
|
return
|
|
else:
|
|
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 or 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 = []
|
|
cstrGroup = self.getConstraintGroup()
|
|
if not cstrGroup:
|
|
return []
|
|
ret = []
|
|
for o in flattenGroup(cstrGroup):
|
|
checkType(o,AsmConstraint)
|
|
if Constraint.isDisabled(o):
|
|
logger.debug('skip constraint {}',cstrName(o))
|
|
continue
|
|
if not System.isConstraintSupported(self.Object,
|
|
Constraint.getTypeName(o)):
|
|
logger.warn('skip unsupported constraint '
|
|
'{}',cstrName(o))
|
|
continue
|
|
ret.append(o)
|
|
self.constraints = ret
|
|
return self.constraints
|
|
|
|
def getElementGroup(self,create=False):
|
|
obj = self.Object
|
|
if create:
|
|
# make sure previous group exists
|
|
self.getConstraintGroup(True)
|
|
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')
|
|
ret = AsmElementGroup.make(obj)
|
|
obj.setLink({1:ret})
|
|
return ret
|
|
|
|
def getPartGroup(self,create=False):
|
|
obj = self.Object
|
|
if create:
|
|
# make sure previous group exists
|
|
self.getElementGroup(True)
|
|
try:
|
|
ret = obj.Group[2]
|
|
checkType(ret,AsmPartGroup)
|
|
parent = getattr(ret.Proxy,'parent',None)
|
|
if not parent:
|
|
ret.Proxy.parent = self
|
|
ret.Proxy.checkDerivedParts()
|
|
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')
|
|
ret = AsmPartGroup.make(obj)
|
|
obj.setLink({2:ret})
|
|
return ret
|
|
|
|
def getRelationGroup(self,create=False):
|
|
obj = self.Object
|
|
if create:
|
|
# make sure previous group exists
|
|
self.getPartGroup(True)
|
|
try:
|
|
ret = obj.Group[3]
|
|
if obj.Freeze:
|
|
if not isTypeOf(ret,AsmRelationGroup):
|
|
return
|
|
else:
|
|
checkType(ret,AsmRelationGroup)
|
|
parent = getattr(ret.Proxy,'parent',None)
|
|
if not parent:
|
|
ret.Proxy.parent = self
|
|
elif parent!=self:
|
|
raise RuntimeError(
|
|
'invalid parent of relation group {}'.format(objName(ret)))
|
|
return ret
|
|
except IndexError:
|
|
if create:
|
|
ret = AsmRelationGroup.make(obj)
|
|
touched = 'Touched' in obj.State
|
|
obj.setLink({3:ret})
|
|
if not touched:
|
|
obj.purgeTouched()
|
|
return ret
|
|
|
|
@staticmethod
|
|
def addOrigin(partGroup, name=None):
|
|
obj = None
|
|
for o in flattenGroup(partGroup):
|
|
if o.TypeId == 'App::Origin':
|
|
obj = o
|
|
break
|
|
if not obj:
|
|
if not name:
|
|
name = 'Origin'
|
|
obj = partGroup.Document.addObject('App::Origin',name)
|
|
partGroup.setLink({-1:obj})
|
|
|
|
partGroup.recompute(True)
|
|
shape = Part.getShape(partGroup)
|
|
if not shape.isNull():
|
|
bbox = shape.BoundBox
|
|
if bbox.isValid():
|
|
obj.ViewObject.Size = tuple([
|
|
max(abs(a),abs(b)) for a,b in (
|
|
(bbox.XMin,bbox.XMax),
|
|
(bbox.YMin,bbox.YMax),
|
|
(bbox.ZMin,bbox.ZMax)) ])
|
|
return obj
|
|
|
|
@staticmethod
|
|
def make(doc=None,name='Assembly',undo=True):
|
|
if not doc:
|
|
doc = FreeCAD.ActiveDocument
|
|
if not doc:
|
|
raise RuntimeError('No active document')
|
|
if undo:
|
|
FreeCAD.setActiveTransaction('Create assembly')
|
|
try:
|
|
obj = doc.addObject("Part::FeaturePython",name,Assembly(),None,True)
|
|
obj.setPropertyStatus('Shape','Transient')
|
|
ViewProviderAssembly(obj.ViewObject)
|
|
obj.Visibility = True
|
|
if gui.AsmCmdManager.AddOrigin:
|
|
Assembly.addOrigin(obj.Proxy.getPartGroup())
|
|
obj.purgeTouched()
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction()
|
|
FreeCADGui.Selection.pushSelStack()
|
|
FreeCADGui.Selection.clearSelection()
|
|
FreeCADGui.Selection.addSelection(obj)
|
|
FreeCADGui.Selection.pushSelStack()
|
|
except Exception:
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
return obj
|
|
|
|
Info = namedtuple('AssemblyInfo',('Assembly','Object','Subname'))
|
|
|
|
@staticmethod
|
|
def getSelection(sels=None):
|
|
'Find all assembly objects among the current selection'
|
|
objs = set()
|
|
if sels is None:
|
|
sels = FreeCADGui.Selection.getSelectionEx('',False)
|
|
for sel in sels:
|
|
if not sel.SubElementNames:
|
|
if isTypeOf(sel.Object,Assembly,True):
|
|
objs.add(sel.Object)
|
|
continue
|
|
for subname in sel.SubElementNames:
|
|
ret = Assembly.find(sel.Object,subname,keepEmptyChild=True)
|
|
if ret:
|
|
objs.add(ret.Assembly)
|
|
return tuple(objs)
|
|
|
|
@staticmethod
|
|
def find(obj,subname,childType=None,
|
|
recursive=False,relativeToChild=True,keepEmptyChild=False):
|
|
'''
|
|
Find the immediate child of the first Assembly referenced in 'subs'
|
|
|
|
obj: the parent object
|
|
|
|
subname: '.' separated 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 relative 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
|
|
if isTypeOf(obj,Assembly,True):
|
|
assembly = obj
|
|
subs = subname if isinstance(subname,list) else subname.split('.')
|
|
i= 0
|
|
for i,name in enumerate(subs[:-1]):
|
|
sobj = obj.getSubObject(name+'.',1)
|
|
if not sobj:
|
|
raise RuntimeError('Cannot find sub-object {}, '
|
|
'{}'.format(objName(obj),name))
|
|
obj = sobj
|
|
if assembly and isTypeOf(obj,childType):
|
|
child = obj
|
|
break
|
|
assembly = obj if isTypeOf(obj,Assembly,True) else None
|
|
|
|
if not child:
|
|
if keepEmptyChild and assembly:
|
|
ret = Assembly.Info(Assembly=assembly,Object=None,Subname='')
|
|
return [ret] if recursive else ret
|
|
return
|
|
|
|
ret = Assembly.Info(Assembly = assembly, Object = child,
|
|
Subname = '.'.join(subs[i+1:] if relativeToChild else subs[i:]))
|
|
|
|
if not recursive:
|
|
return ret
|
|
|
|
nret = Assembly.find(child, subs[i+1:], childType, recursive,
|
|
relativeToChild, keepEmptyChild)
|
|
if nret:
|
|
return [ret] + nret
|
|
return [ret]
|
|
|
|
@staticmethod
|
|
def findChildren(obj,subname,tp=None):
|
|
return Assembly.find(obj,subname,tp,True,False,True)
|
|
|
|
@staticmethod
|
|
def findPartGroup(obj,subname='2.',recursive=False,relativeToChild=True):
|
|
return Assembly.find(
|
|
obj,subname,AsmPartGroup,recursive,relativeToChild)
|
|
|
|
@staticmethod
|
|
def findElementGroup(obj,subname='1.',relativeToChild=True):
|
|
return Assembly.find(
|
|
obj,subname,AsmElementGroup,False,relativeToChild)
|
|
|
|
@staticmethod
|
|
def findConstraintGroup(obj,subname='0.',relativeToChild=True):
|
|
return Assembly.find(
|
|
obj,subname,AsmConstraintGroup,False,relativeToChild)
|
|
|
|
@staticmethod
|
|
def fromLinkGroup(obj):
|
|
block = gui.AsmCmdManager.AutoRecompute
|
|
if block:
|
|
gui.AsmCmdManager.AutoRecompute = False
|
|
try:
|
|
removes = set()
|
|
table = {}
|
|
asm = Assembly._fromLinkGroup(obj,table,removes)
|
|
for o in removes:
|
|
o.Document.removeObject(o.Name)
|
|
asm.recompute(True)
|
|
return asm
|
|
finally:
|
|
if block:
|
|
gui.AsmCmdManager.AutoRecompute = True
|
|
|
|
@staticmethod
|
|
def _fromLinkGroup(obj,table,removes):
|
|
mapped = table.get(obj,None)
|
|
if mapped:
|
|
return mapped
|
|
|
|
if hasProperty(obj,'Shape'):
|
|
return obj
|
|
|
|
linked = obj.getLinkedObject(False)
|
|
if linked==obj and getattr(obj,'ElementCount',0):
|
|
linked = obj.LinkedObject
|
|
|
|
if linked != obj:
|
|
mapped = Assembly._fromLinkGroup(linked,table,removes)
|
|
if mapped != linked:
|
|
obj.setLink(mapped)
|
|
table[obj] = obj
|
|
return obj
|
|
|
|
children = []
|
|
hiddens = []
|
|
subs = obj.getSubObjects()
|
|
for sub in subs:
|
|
child,parent,childName,_ = obj.resolve(sub)
|
|
if not child:
|
|
logger.warn('failed to find sub object {}.{}'.format(
|
|
obj.Name,sub))
|
|
continue
|
|
asm = Assembly._fromLinkGroup(child,table,removes)
|
|
children.append(asm)
|
|
if not parent.isElementVisible(childName):
|
|
hiddens.append(asm.Name)
|
|
asm.Visibility = False
|
|
|
|
asm = Assembly.make(obj.Document,undo=False)
|
|
asm.Label = obj.Label
|
|
asm.Placement = obj.Placement
|
|
partGroup = asm.Proxy.getPartGroup()
|
|
partGroup.setLink(children)
|
|
for sub in hiddens:
|
|
partGroup.setElementVisible(sub,False)
|
|
table[obj] = asm
|
|
removes.add(obj)
|
|
return asm
|
|
|
|
class ViewProviderAssembly(ViewProviderAsmGroup):
|
|
_iconName = 'Assembly_Assembly_Frozen_Tree.svg'
|
|
|
|
def __init__(self,vobj):
|
|
self._movingPart = None
|
|
super(ViewProviderAssembly,self).__init__(vobj)
|
|
self.showParts()
|
|
|
|
def setupContextMenu(self,vobj,menu):
|
|
obj = vobj.Object
|
|
action = QtGui.QAction(QtGui.QIcon(),
|
|
"Unfreeze assembly" if obj.Freeze else "Freeze assembly", menu)
|
|
QtCore.QObject.connect(
|
|
action,QtCore.SIGNAL("triggered()"),self.toggleFreeze)
|
|
menu.addAction(action)
|
|
|
|
def toggleFreeze(self):
|
|
obj = self.ViewObject.Object
|
|
FreeCAD.setActiveTransaction(
|
|
'Unfreeze assembly' if obj.Freeze else 'Freeze assembly')
|
|
try:
|
|
obj.Freeze = not obj.Freeze
|
|
obj.recompute(True)
|
|
FreeCAD.closeActiveTransaction()
|
|
except Exception:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
|
|
def canAddToSceneGraph(self):
|
|
return True
|
|
|
|
def onDelete(self,vobj,_subs):
|
|
assembly = vobj.Object.Proxy
|
|
for o in assembly.getPartGroup().LinkedChildren:
|
|
if o.isDerivedFrom('App::Origin'):
|
|
o.Document.removeObject(o.Name)
|
|
break
|
|
return True
|
|
|
|
def canDelete(self,obj):
|
|
return isTypeOf(obj,AsmRelationGroup)
|
|
|
|
def _convertSubname(self,owner,subname):
|
|
sub = subname.split('.')
|
|
if not sub:
|
|
return
|
|
me = self.ViewObject.Object
|
|
partGroup = me.Proxy.getPartGroup().ViewObject
|
|
if sub[0] == me.Name:
|
|
return partGroup,partGroup,subname[len(sub[0])+1:]
|
|
return partGroup,owner,subname
|
|
|
|
def canDropObjectEx(self,obj,owner,subname,_elements):
|
|
info = self._convertSubname(owner,subname)
|
|
if not info:
|
|
return False
|
|
partGroup,owner,subname = info
|
|
return partGroup.canDropObject(obj,owner,subname)
|
|
|
|
def canDragAndDropObject(self,_obj):
|
|
return True
|
|
|
|
def dropObjectEx(self,_vobj,obj,owner,subname,_elements):
|
|
info = self._convertSubname(owner,subname)
|
|
if not info:
|
|
return False
|
|
partGroup,owner,subname = info
|
|
return '2.{}'.format(partGroup.dropObject(obj,owner,subname))
|
|
|
|
def getDropPrefix(self):
|
|
return '2.'
|
|
|
|
def getIcon(self):
|
|
if getattr(self.ViewObject.Object,'Freeze',False):
|
|
return utils.getIcon(self.__class__)
|
|
return System.getIcon(self.ViewObject.Object)
|
|
|
|
def doubleClicked(self, vobj):
|
|
from . import mover
|
|
sel = FreeCADGui.Selection.getSelection('',0)
|
|
if not sel:
|
|
return False
|
|
if sel[0].getLinkedObject(True) == vobj.Object:
|
|
vobj = sel[0].ViewObject
|
|
return vobj.Document.setEdit(vobj,1)
|
|
if logger.catchDebug('',mover.movePart):
|
|
return True
|
|
return False
|
|
|
|
def onExecute(self):
|
|
if not getattr(self,'_movingPart',None):
|
|
return
|
|
|
|
pla = logger.catch('exception when update moving part',
|
|
self._movingPart.update)
|
|
if pla:
|
|
self.ViewObject.DraggingPlacement = pla
|
|
return
|
|
|
|
# Must NOT call resetEdit() here. Because we are called through dragger
|
|
# callback, meaning that we are called during coin node traversal.
|
|
# resetEdit() will cause View3DInventorView to reset editing root node.
|
|
# And disaster will happen when modifying coin node tree while
|
|
# traversing.
|
|
#
|
|
# doc = FreeCADGui.editDocument()
|
|
# if doc:
|
|
# doc.resetEdit()
|
|
|
|
def initDraggingPlacement(self):
|
|
if not getattr(self,'_movingPart',None):
|
|
return True
|
|
self._movingPart.begin()
|
|
return (FreeCADGui.editDocument().EditingTransform,
|
|
self._movingPart.draggerPlacement,
|
|
self._movingPart.bbox)
|
|
|
|
_Busy = False
|
|
|
|
def onDragStart(self):
|
|
Assembly.cancelAutoSolve();
|
|
FreeCADGui.Selection.clearSelection()
|
|
self.__class__._Busy = True
|
|
if getattr(self,'_movingPart',None):
|
|
FreeCAD.setActiveTransaction('Assembly move')
|
|
return True
|
|
|
|
def onDragMotion(self):
|
|
if getattr(self,'_movingPart',None):
|
|
self._movingPart.move()
|
|
return True
|
|
|
|
def onDragEnd(self):
|
|
try:
|
|
if getattr(self,'_movingPart',None):
|
|
pla = self._movingPart.dragEnd()
|
|
if pla:
|
|
self.ViewObject.DraggingPlacement = pla
|
|
FreeCAD.closeActiveTransaction()
|
|
return True
|
|
finally:
|
|
self.__class__._Busy = False
|
|
|
|
def unsetEdit(self,_vobj,_mode):
|
|
if self._movingPart:
|
|
self._movingPart.end()
|
|
self._movingPart = None
|
|
return False
|
|
|
|
def showParts(self):
|
|
if not hasProperty(self.ViewObject,'ShowParts'):
|
|
self.ViewObject.addProperty("App::PropertyBool","ShowParts"," Link")
|
|
return
|
|
proxy = self.ViewObject.Object.Proxy
|
|
if proxy:
|
|
proxy.getPartGroup().ViewObject.Proxy.showParts()
|
|
|
|
def updateData(self,_obj,prop):
|
|
if not hasattr(self,'ViewObject') or FreeCAD.isRestoring():
|
|
return
|
|
if prop=='Freeze':
|
|
self.showParts()
|
|
self.ViewObject.signalChangeIcon()
|
|
elif prop=='BuildShape':
|
|
self.showParts()
|
|
|
|
def onChanged(self,_vobj,prop):
|
|
if not hasattr(self,'ViewObject') or FreeCAD.isRestoring():
|
|
return
|
|
if prop=='ShowParts':
|
|
self.showParts()
|
|
|
|
def finishRestoring(self):
|
|
self.showParts()
|
|
|
|
@classmethod
|
|
def isBusy(cls):
|
|
return cls._Busy
|
|
|
|
|
|
class AsmWorkPlane(object):
|
|
def __init__(self,obj):
|
|
obj.addProperty("App::PropertyLength","Length","Base")
|
|
obj.addProperty("App::PropertyLength","Width","Base")
|
|
obj.addProperty("App::PropertyBool","Fixed","Base")
|
|
obj.Fixed = True
|
|
obj.Length = 10
|
|
obj.Width = 10
|
|
obj.Proxy = self
|
|
|
|
def execute(self,obj):
|
|
length = obj.Length.Value
|
|
width = obj.Width.Value
|
|
if not length:
|
|
if not width:
|
|
obj.Shape = Part.Vertex(FreeCAD.Vector())
|
|
else:
|
|
obj.Shape = Part.makeLine(FreeCAD.Vector(0,-width/2,0),
|
|
FreeCAD.Vector(0,width/2,0))
|
|
elif not width:
|
|
obj.Shape = Part.makeLine(FreeCAD.Vector(-length/2,0,0),
|
|
FreeCAD.Vector(length/2,0,0))
|
|
else:
|
|
obj.Shape = Part.makePlane(length,width,
|
|
FreeCAD.Vector(-length/2,-width/2,0))
|
|
|
|
def __getstate__(self):
|
|
return
|
|
|
|
def __setstate__(self,_state):
|
|
return
|
|
|
|
Info = namedtuple('AsmWorkPlaneSelectionInfo',
|
|
('SelObj','SelSubname','PartGroup','Placement','Shape','BoundBox'))
|
|
|
|
@staticmethod
|
|
def getSelection(sels=None):
|
|
if not sels:
|
|
sels = FreeCADGui.Selection.getSelectionEx('',False)
|
|
if not sels:
|
|
raise RuntimeError('no selection')
|
|
elements = []
|
|
objs = []
|
|
for sel in sels:
|
|
if not sel.SubElementNames:
|
|
elements.append((sel.Object,''))
|
|
if len(elements) > 2:
|
|
raise RuntimeError('Too many selection')
|
|
objs.append(sel.Object)
|
|
continue
|
|
for sub in sel.SubElementNames:
|
|
elements.append((sel.Object,sub))
|
|
if len(elements) > 2:
|
|
raise RuntimeError('Too many selection')
|
|
objs.append(sel.Object.getSubObject(sub,1))
|
|
if len(elements)==2:
|
|
if isTypeOf(objs[0],Assembly,True):
|
|
assembly = objs[0]
|
|
selObj,sub = elements[0]
|
|
element = elements[1]
|
|
elif isTypeOf(objs[1],Assembly,True):
|
|
assembly = objs[1]
|
|
selObj,sub = elements[1]
|
|
element = elements[0]
|
|
else:
|
|
raise RuntimeError('For two selections, one of the selections '
|
|
'must be of an assembly container')
|
|
_,mat = selObj.getSubObject(sub,1,FreeCAD.Matrix())
|
|
shape = utils.getElementShape(element,transform=True)
|
|
bbox = shape.BoundBox
|
|
pla = utils.getElementPlacement(shape,mat)
|
|
else:
|
|
shape = None
|
|
element = elements[0]
|
|
ret = Assembly.find(element[0],element[1],
|
|
relativeToChild=False,keepEmptyChild=True)
|
|
if not ret:
|
|
raise RuntimeError('Single selection must be an assembly or '
|
|
'an object inside of an assembly')
|
|
assembly = ret.Assembly
|
|
sub = element[1][:-len(ret.Subname)]
|
|
selObj = element[0]
|
|
if not ret.Subname:
|
|
pla = FreeCAD.Placement()
|
|
bbox = assembly.ViewObject.getBoundingBox()
|
|
else:
|
|
shape = utils.getElementShape((assembly,ret.Subname),
|
|
transform=True)
|
|
bbox = shape.BoundBox
|
|
pla = utils.getElementPlacement(shape,
|
|
ret.Assembly.Placement.toMatrix())
|
|
|
|
return AsmWorkPlane.Info(
|
|
SelObj = selObj,
|
|
SelSubname = sub,
|
|
PartGroup = resolveAssembly(assembly).getPartGroup(),
|
|
Shape = shape,
|
|
Placement = pla,
|
|
BoundBox = bbox)
|
|
|
|
@staticmethod
|
|
def make(info=None,name=None, tp=0, undo=True):
|
|
if not info:
|
|
info = AsmWorkPlane.getSelection()
|
|
doc = info.PartGroup.Document
|
|
if undo:
|
|
FreeCAD.setActiveTransaction('Assembly create workplane')
|
|
try:
|
|
logger.debug('make {}',tp)
|
|
if tp == 3:
|
|
obj = Assembly.addOrigin(info.PartGroup,name)
|
|
else:
|
|
if tp==1:
|
|
pla = FreeCAD.Placement(info.Placement.Base,
|
|
FreeCAD.Rotation(FreeCAD.Vector(0,1,0),-90))
|
|
elif tp==2:
|
|
pla = FreeCAD.Placement(info.Placement.Base,
|
|
FreeCAD.Rotation(FreeCAD.Vector(1,0,0),90))
|
|
else:
|
|
pla = info.Placement
|
|
|
|
if tp == 4:
|
|
if not name:
|
|
name = 'Placement'
|
|
obj = doc.addObject('App::Placement',name)
|
|
elif not name:
|
|
name = 'Workplane'
|
|
obj = doc.addObject('Part::FeaturePython',name)
|
|
AsmWorkPlane(obj)
|
|
ViewProviderAsmWorkPlane(obj.ViewObject)
|
|
if utils.isVertex(info.Shape):
|
|
obj.Length = obj.Width = 0
|
|
elif utils.isLinearEdge(info.Shape):
|
|
if info.BoundBox.isValid():
|
|
obj.Length = info.BoundBox.DiagonalLength
|
|
obj.Width = 0
|
|
pla = FreeCAD.Placement(pla.Base,pla.Rotation.multiply(
|
|
FreeCAD.Rotation(FreeCAD.Vector(0,1,0),90)))
|
|
elif info.BoundBox.isValid():
|
|
obj.Length = obj.Width = info.BoundBox.DiagonalLength
|
|
|
|
obj.Placement = pla
|
|
|
|
obj.recompute(True)
|
|
info.PartGroup.setLink({-1:obj})
|
|
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction()
|
|
|
|
FreeCADGui.Selection.clearSelection()
|
|
FreeCADGui.Selection.addSelection(info.SelObj,
|
|
info.SelSubname + info.PartGroup.Name + '.' + obj.Name + '.')
|
|
FreeCADGui.runCommand('Std_TreeSelection')
|
|
FreeCADGui.Selection.setVisible(True)
|
|
return obj
|
|
except Exception:
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
|
|
|
|
class ViewProviderAsmWorkPlane(ViewProviderAsmBase):
|
|
_iconName = 'Assembly_Workplane.svg'
|
|
|
|
def __init__(self,vobj):
|
|
vobj.Transparency = 50
|
|
color = (0.0,0.33,1.0,1.0)
|
|
vobj.LineColor = color
|
|
vobj.PointColor = color
|
|
vobj.OnTopWhenSelected = 1
|
|
super(ViewProviderAsmWorkPlane,self).__init__(vobj)
|
|
|
|
def canDropObjects(self):
|
|
return False
|
|
|
|
def getDisplayModes(self, _vobj):
|
|
modes=[]
|
|
return modes
|
|
|
|
def setDisplayMode(self, mode):
|
|
return mode
|
|
|
|
|
|
class AsmPlainGroup(object):
|
|
def __init__(self,obj,parent):
|
|
obj.addProperty("App::PropertyLinkHidden","_Parent"," Link",'')
|
|
obj._Parent = parent
|
|
obj.setPropertyStatus('_Parent',('Hidden','Immutable'))
|
|
obj.Proxy = self
|
|
|
|
def __getstate__(self):
|
|
return
|
|
|
|
def __setstate__(self,_state):
|
|
return
|
|
|
|
@staticmethod
|
|
def getParentGroup(obj):
|
|
for o in obj.InList:
|
|
if isTypeOf(o,(AsmGroup,AsmPlainGroup)):
|
|
return o
|
|
|
|
@staticmethod
|
|
def contains(parent,obj):
|
|
return obj in getattr(parent,'_ChildCache',[])
|
|
|
|
@staticmethod
|
|
def tryMove(obj,toGroup):
|
|
group = AsmPlainGroup.getParentGroup(obj)
|
|
if not group or group is toGroup:
|
|
return False
|
|
if isTypeOf(group,AsmPlainGroup):
|
|
parent = getattr(group,'_Parent', None)
|
|
else:
|
|
parent = group
|
|
if isTypeOf(toGroup,AsmPlainGroup):
|
|
if getattr(toGroup,'_Parent',None) is not parent:
|
|
return False
|
|
elif toGroup is not parent:
|
|
return False
|
|
children = group.Group
|
|
children.remove(obj)
|
|
editGroup(group,children)
|
|
children = toGroup.Group
|
|
children.append(obj)
|
|
editGroup(toGroup,children)
|
|
return True
|
|
|
|
# SelObj: selected top object
|
|
# SelSubname: subname refercing the last common parent of the selections
|
|
# Parent: sub-group of the parent assembly
|
|
# Group: immediate group of all selected objects, may or may not be the
|
|
# same as 'Parent'
|
|
# Objects: selected objects
|
|
Info = namedtuple('AsmPlainGroupSelectionInfo',
|
|
('SelObj','SelSubname','Parent','Group','Objects'))
|
|
|
|
@staticmethod
|
|
def getSelection(sels=None):
|
|
if not sels:
|
|
sels = FreeCADGui.Selection.getSelectionEx('',False)
|
|
if not sels:
|
|
raise RuntimeError('no selection')
|
|
elif len(sels)>1:
|
|
raise RuntimeError('Too many selection')
|
|
sel = sels[0]
|
|
if not sel.SubElementNames:
|
|
raise RuntimeError('Invalid selection')
|
|
|
|
parent = None
|
|
subs = []
|
|
for sub in sel.SubElementNames:
|
|
h = Assembly.find(sel.Object,sub,recursive=True,
|
|
childType=(AsmConstraintGroup,AsmElementGroup,AsmPartGroup))
|
|
if not h:
|
|
raise RuntimeError("Invalid selection {}.{}".format(
|
|
objName(sel.Object),sub))
|
|
h = h[-1]
|
|
if not parent:
|
|
parent = h.Object
|
|
selSub = sub[:-len(h.Subname)]
|
|
elif parent != h.Object:
|
|
raise RuntimeError("Selection from different assembly")
|
|
subs.append(h.Subname)
|
|
|
|
if len(subs) == 1:
|
|
group = parent
|
|
common = ''
|
|
sub = subs[0]
|
|
end = len(sub)
|
|
lastObj = None
|
|
while True:
|
|
index = sub.rfind('.',0,end)
|
|
if index<0:
|
|
break
|
|
end = index-1
|
|
sobj = group.getSubObject(sub[:index+1],1)
|
|
if not sobj:
|
|
raise RuntimeError('Sub object not found: {}.{}'.format(
|
|
objName(group),sub))
|
|
if lastObj and isTypeOf(sobj,AsmPlainGroup):
|
|
group = sobj
|
|
selSub += sub[:index+1]
|
|
subs[0] = sub[index+1:]
|
|
break
|
|
lastObj = sobj
|
|
else:
|
|
common = os.path.commonprefix(subs)
|
|
idx = common.rfind('.')
|
|
if idx<0:
|
|
group = parent
|
|
common = ''
|
|
else:
|
|
common = common[:idx+1]
|
|
group = parent.getSubObject(common,1)
|
|
if not group:
|
|
raise RuntimeError('Sub object not found: {}.{}'.format(
|
|
objName(parent),common))
|
|
if not isTypeOf(group,AsmPlainGroup):
|
|
raise RuntimeError('Not from plain group')
|
|
selSub += common
|
|
subs = [ s[idx+1:] for s in subs ]
|
|
objs = []
|
|
for s in subs:
|
|
sub = s[:s.index('.')+1]
|
|
if not sub:
|
|
raise RuntimeError('Invalid subname: {}.{}{}'.format(
|
|
objName(parent),common,s))
|
|
sobj = group.getSubObject(sub,1)
|
|
if not sobj:
|
|
raise RuntimeError('Sub object not found: {}.{}'.format(
|
|
objName(group),sub))
|
|
if sobj not in objs:
|
|
objs.append(sobj)
|
|
|
|
return AsmPlainGroup.Info(SelObj=sel.Object,
|
|
SelSubname=selSub,
|
|
Parent=parent,
|
|
Group=group,
|
|
Objects=objs)
|
|
|
|
@staticmethod
|
|
def make(sels=None,name=None, undo=True):
|
|
info = AsmPlainGroup.getSelection(sels)
|
|
doc = info.Parent.Document
|
|
if undo:
|
|
FreeCAD.setActiveTransaction('Assembly create group')
|
|
try:
|
|
if not name:
|
|
name = 'Group'
|
|
obj = doc.addObject('App::DocumentObjectGroupPython',name)
|
|
AsmPlainGroup(obj,info.Parent)
|
|
ViewProviderAsmPlainGroup(obj.ViewObject)
|
|
group = info.Group.Group
|
|
indices = [ group.index(o) for o in info.Objects ]
|
|
indices.sort()
|
|
child = group[indices[0]]
|
|
group = [ o for o in info.Group.Group
|
|
if o not in info.Objects ]
|
|
group.insert(indices[0],obj)
|
|
|
|
notouch = indices[-1] == indices[0]+len(indices)-1
|
|
editGroup(info.Group,group,notouch)
|
|
obj.purgeTouched()
|
|
editGroup(obj,info.Objects,notouch)
|
|
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction()
|
|
|
|
FreeCADGui.Selection.clearSelection()
|
|
FreeCADGui.Selection.addSelection(info.SelObj,'{}{}.{}.'.format(
|
|
info.SelSubname,obj.Name,child.Name))
|
|
FreeCADGui.runCommand('Std_TreeSelection')
|
|
return obj
|
|
except Exception:
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
|
|
class ViewProviderAsmPlainGroup(object):
|
|
def __init__(self,vobj):
|
|
vobj.Visibility = False
|
|
vobj.Proxy = self
|
|
self.attach(vobj)
|
|
|
|
def attach(self,vobj):
|
|
if hasattr(self,'ViewObject'):
|
|
return
|
|
self.ViewObject = vobj
|
|
vobj.setPropertyStatus('Visibility','Hidden')
|
|
|
|
def __getstate__(self):
|
|
return None
|
|
|
|
def __setstate__(self, _state):
|
|
return None
|
|
|
|
def onDelete(self,vobj,_subs):
|
|
obj = vobj.Object
|
|
group = AsmPlainGroup.getParentGroup(obj)
|
|
if group:
|
|
children = group.Group
|
|
idx = children.index(obj)
|
|
children = children[:idx] + obj.Group + children[idx+1:]
|
|
editGroup(obj,[],True)
|
|
editGroup(group,children,True)
|
|
return True
|
|
|
|
def setupContextMenu(self,_vobj,menu):
|
|
setupSortMenu(menu,self.sort,self.sortReverse)
|
|
|
|
def sortReverse(self):
|
|
sortChildren(self.ViewObject.Object,True)
|
|
|
|
def sort(self):
|
|
sortChildren(self.ViewObject.Object,False)
|
|
|
|
def canDragAndDropObject(self,_obj):
|
|
return False
|
|
|
|
def canDropObjects(self):
|
|
return True
|
|
|
|
def canDropObjectEx(self,obj,owner,subname,elements):
|
|
parent = getattr(self.ViewObject.Object,'_Parent',None)
|
|
if not parent:
|
|
return False
|
|
if AsmPlainGroup.contains(parent,obj):
|
|
return True
|
|
return parent.ViewObject.canDropObject(obj,owner,subname,elements)
|
|
|
|
def dropObjectEx(self,vobj,obj,owner,subname,elements):
|
|
if AsmPlainGroup.tryMove(obj,vobj.Object):
|
|
return
|
|
parent = getattr(vobj.Object,'_Parent',None)
|
|
if not parent:
|
|
return
|
|
func = getattr(parent.ViewObject.Proxy,'_drop',None)
|
|
if func:
|
|
group = parent.Group
|
|
children = func(obj,owner,subname,elements)
|
|
children = vobj.Object.Group + children
|
|
editGroup(parent,group)
|
|
editGroup(vobj.Object,children)
|