
When enabled, it will hide all AsmElement, and AsmElementLink, and only shown then when they are selected, or a AsmConstraint object is selected in the tree view
1765 lines
61 KiB
Python
1765 lines
61 KiB
Python
import os
|
|
from collections import namedtuple
|
|
import FreeCAD, FreeCADGui, Part
|
|
from PySide import QtCore, QtGui
|
|
from . import utils, gui
|
|
from .utils import 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 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','_LinkRecomputed'])
|
|
|
|
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 attach(self,vobj):
|
|
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.GroupMode = 1 # auto delete children
|
|
self.Object.setPropertyStatus('GroupMode','Hidden')
|
|
self.Object.setPropertyStatus('GroupMode','Immutable')
|
|
self.Object.setPropertyStatus('GroupMode','Transient')
|
|
self.Object.setPropertyStatus('Group','Hidden')
|
|
self.Object.setPropertyStatus('Group','Immutable')
|
|
self.Object.setPropertyStatus('VisibilityList','Output')
|
|
|
|
def attach(self,obj):
|
|
obj.addProperty("App::PropertyLinkList","Group","Base",'')
|
|
obj.addProperty("App::PropertyBoolList","VisibilityList","Base",'')
|
|
obj.addProperty("App::PropertyEnumeration","GroupMode","Base",'')
|
|
super(AsmGroup,self).attach(obj)
|
|
|
|
|
|
class ViewProviderAsmGroup(ViewProviderAsmBase):
|
|
def claimChildren(self):
|
|
return self.ViewObject.Object.Group
|
|
|
|
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)
|
|
super(AsmPartGroup,self).__init__()
|
|
|
|
def getAssembly(self):
|
|
return self.parent
|
|
|
|
def groupSetup(self):
|
|
pass
|
|
|
|
@staticmethod
|
|
def make(parent,name='Parts'):
|
|
obj = parent.Document.addObject("App::FeaturePython",name,
|
|
AsmPartGroup(parent),None,True)
|
|
ViewProviderAsmPartGroup(obj.ViewObject)
|
|
obj.purgeTouched()
|
|
return obj
|
|
|
|
|
|
class ViewProviderAsmPartGroup(ViewProviderAsmGroup):
|
|
_iconName = 'Assembly_Assembly_Part_Tree.svg'
|
|
|
|
def onDelete(self,_obj,_subs):
|
|
return False
|
|
|
|
def canDropObjectEx(self,obj,_owner,_subname):
|
|
return isTypeOf(obj,Assembly, True) or not isTypeOf(obj,AsmBase)
|
|
|
|
def canDragObject(self,_obj):
|
|
return True
|
|
|
|
def canDragObjects(self):
|
|
return True
|
|
|
|
def canDragAndDropObject(self,_obj):
|
|
return True
|
|
|
|
|
|
class AsmElement(AsmBase):
|
|
def __init__(self,parent):
|
|
self._initializing = True
|
|
self.shape = None
|
|
self.parent = getProxy(parent,AsmElementGroup)
|
|
super(AsmElement,self).__init__()
|
|
|
|
def linkSetup(self,obj):
|
|
super(AsmElement,self).linkSetup(obj)
|
|
obj.configLinkProperty('LinkedObject')
|
|
# obj.setPropertyStatus('LinkedObject','Immutable')
|
|
obj.setPropertyStatus('LinkedObject','ReadOnly')
|
|
|
|
def attach(self,obj):
|
|
obj.addProperty("App::PropertyXLink","LinkedObject"," Link",'')
|
|
super(AsmElement,self).attach(obj)
|
|
|
|
def 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 onChanged(self,_obj,prop):
|
|
parent = getattr(self,'parent',None)
|
|
if parent and \
|
|
not getattr(self,'_initializing',False) and \
|
|
prop=='Label':
|
|
parent.Object.cacheChildLabel()
|
|
|
|
def execute(self,_obj):
|
|
# self.getShape(True)
|
|
return False
|
|
|
|
def getShape(self,refresh=False):
|
|
if not refresh:
|
|
ret = getattr(self,'shape',None)
|
|
if ret:
|
|
return ret
|
|
self.shape = None
|
|
self.shape = self.Object.getSubObject('')
|
|
return self.shape
|
|
|
|
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 isinstance(link,tuple):
|
|
raise RuntimeError('Invalid element link "{}"'.format(
|
|
objName(self.Object)))
|
|
return link[1]
|
|
|
|
def getElementSubname(self):
|
|
'''
|
|
Resolve the geometry element link relative to the parent assembly's part
|
|
group
|
|
'''
|
|
|
|
subname = self.getSubName()
|
|
obj = self.Object.getLinkedObject(False)
|
|
if not obj or obj == self.Object:
|
|
raise RuntimeError('Borken element link')
|
|
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+childElement.getAssembly().getPartGroup().Name+'.'+\
|
|
childElement.getElementSubname()
|
|
|
|
# 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 realtive to 'Group'
|
|
Selection = namedtuple('AsmElementSelection',('Element','Group','Subname'))
|
|
|
|
@staticmethod
|
|
def getSelection():
|
|
'''
|
|
Parse Gui.Selection for making an element
|
|
|
|
If there is only one selection, then the selection must refer to a sub
|
|
element of some part object of an assembly. We shall create a new
|
|
element beloning to the top-level assembly
|
|
|
|
If there are two selections, then first one shall be either the
|
|
element group or an individual element. The second selection shall
|
|
be a sub element belong to a child assembly of the parent assembly of
|
|
the first selected element/element group
|
|
'''
|
|
sels = FreeCADGui.Selection.getSelectionEx('',False)
|
|
if not sels:
|
|
return
|
|
if len(sels)>1:
|
|
raise RuntimeError(
|
|
'The selections must have a common (grand)parent assembly')
|
|
|
|
sel = sels[0]
|
|
subs = list(sel.SubElementNames)
|
|
if not subs:
|
|
raise RuntimeError('no sub object in selection')
|
|
if len(subs)>2:
|
|
raise RuntimeError('At most two selection is allowed.\n'
|
|
'The first selection must be a sub element belonging to some '
|
|
'assembly. The optional second selection must be an element '
|
|
'belonging to the same assembly of the first selection')
|
|
if len(subs)==2:
|
|
if len(subs[0])<len(subs[1]):
|
|
subs = [subs[1],subs[2]]
|
|
|
|
if subs[0][-1] == '.':
|
|
if not utils.isElement((sel.Object,subs[0])):
|
|
raise RuntimeError('no sub element (face, edge, vertex) in '
|
|
'{}.{}'.format(sel.Object.Name,subs[0]))
|
|
subElement = utils.deduceSelectedElement(sel.Object,subs[0])
|
|
if subElement:
|
|
subs[0] += subElement
|
|
|
|
link = Assembly.findPartGroup(sel.Object,subs[0])
|
|
if not link:
|
|
raise RuntimeError(
|
|
'Selected sub element does not belong to an assembly')
|
|
|
|
element = None
|
|
if len(subs)>1:
|
|
ret = Assembly.findElementGroup(sel.Object,subs[1])
|
|
if not ret:
|
|
raise RuntimeError('The second selection must be an element')
|
|
|
|
if ret.Assembly != link.Assembly:
|
|
raise RuntimeError(
|
|
'The two selections must belong to the same assembly')
|
|
|
|
element = ret.Object.getSubObject(ret.Subname,1)
|
|
if not isTypeOf(element,AsmElement):
|
|
raise RuntimeError('The second selection must be an element')
|
|
|
|
return AsmElement.Selection(Element=element, Group=link.Object,
|
|
Subname=link.Subname+subElement)
|
|
|
|
@staticmethod
|
|
def make(selection=None,name='Element',undo=False):
|
|
'''Add/get/modify an element with the given selected object'''
|
|
if not selection:
|
|
selection = AsmElement.getSelection()
|
|
|
|
group = selection.Group
|
|
subname = 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))
|
|
return element
|
|
|
|
if 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):
|
|
raise RuntimeError('Invalid link reference {}.{}'.format(
|
|
group.Name,subname))
|
|
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.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')
|
|
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(
|
|
Element=None, Group=ret.Object, Subname=ret.Subname)
|
|
element = AsmElement.make(sel)
|
|
|
|
# now generate the subname reference
|
|
|
|
# This give us reference to child assembly's immediate child
|
|
# without trailing dot.
|
|
prefix = subname[:len(subname)-len(ret.Subname)-1]
|
|
|
|
# Pop the immediate child name, and replace it with child
|
|
# assembly's element group name
|
|
prefix = prefix[:prefix.rfind('.')+1] + \
|
|
resolveAssembly(ret.Assembly).getElementGroup().Name
|
|
|
|
subname = '{}.${}.'.format(prefix,element.Label)
|
|
|
|
else:
|
|
raise RuntimeError('Invalid selection {}.{}'.format(
|
|
group.Name,subname))
|
|
|
|
element = selection.Element
|
|
|
|
try:
|
|
if undo:
|
|
FreeCAD.setActiveTransaction('Assembly change element' \
|
|
if element else 'Assembly create element')
|
|
if not element:
|
|
elements = group.Proxy.getAssembly().getElementGroup()
|
|
# try to search the element group for an existing element
|
|
for e in elements.Group:
|
|
sub = logger.catch('',e.Proxy.getSubName)
|
|
if sub == subname:
|
|
return e
|
|
element = elements.Document.addObject("App::FeaturePython",
|
|
name,AsmElement(elements),None,True)
|
|
ViewProviderAsmElement(element.ViewObject)
|
|
elements.setLink({-1:element})
|
|
elements.setElementVisible(element.Name,False)
|
|
element.Proxy._initializing = False
|
|
elements.cacheChildLabel()
|
|
element.setLink(group,subname)
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction()
|
|
except Exception:
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
return element
|
|
|
|
|
|
class ViewProviderAsmElement(ViewProviderAsmOnTop):
|
|
def __init__(self,vobj):
|
|
vobj.OverrideMaterial = True
|
|
vobj.ShapeMaterial.DiffuseColor = self.getDefaultColor()
|
|
vobj.ShapeMaterial.EmissiveColor = self.getDefaultColor()
|
|
vobj.DrawStyle = 1
|
|
vobj.LineWidth = 4
|
|
vobj.PointSize = 8
|
|
super(ViewProviderAsmElement,self).__init__(vobj)
|
|
|
|
def attach(self,vobj):
|
|
super(ViewProviderAsmElement,self).attach(vobj)
|
|
if gui.AsmCmdManager.AutoElementVis:
|
|
vobj.OnTopWhenSelected = 2
|
|
|
|
def getDefaultColor(self):
|
|
return (60.0/255.0,1.0,1.0)
|
|
|
|
def canDropObjectEx(self,_obj,owner,subname):
|
|
if not subname:
|
|
return False
|
|
proxy = self.ViewObject.Object.Proxy
|
|
return proxy.getAssembly().getPartGroup()==owner
|
|
|
|
def dropObjectEx(self,vobj,_obj,owner,subname):
|
|
AsmElement.make(AsmElement.Selection(Element=vobj.Object,
|
|
Group=owner, Subname=subname),undo=True)
|
|
|
|
ElementInfo = namedtuple('AsmElementInfo', ('Parent','SubnameRef','Part',
|
|
'PartName','Placement','Object','Subname','Shape'))
|
|
|
|
def getElementInfo(parent, subname):
|
|
'''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)
|
|
|
|
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(obj, idx) 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
|
|
realtive 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
|
|
|
|
names = subname.split('.')
|
|
if isTypeOf(parent,Assembly,True):
|
|
partGroup = None
|
|
child = parent.getSubObject(names[0]+'.',1)
|
|
if isTypeOf(child,(AsmElementGroup,AsmConstraintGroup)):
|
|
child = parent.getSubObject(subname,1)
|
|
if not child:
|
|
raise RuntimeError('Invalid sub object {}, {}'.format(
|
|
objName(parent), subname))
|
|
if not isTypeOf(child,(AsmElement,AsmElementLink)):
|
|
raise RuntimeError('{} cannot be moved'.format(objName(child)))
|
|
subname = child.Proxy.getElementSubname()
|
|
names = subname.split('.')
|
|
partGroup = parent.Proxy.getPartGroup()
|
|
|
|
elif isTypeOf(child,AsmPartGroup):
|
|
partGroup = child
|
|
names = names[1:]
|
|
subname = '.'.join(names)
|
|
|
|
if not partGroup:
|
|
raise RuntimeError('Invalid sub object {}, {}'.format(
|
|
objName(parent), subname))
|
|
|
|
elif isTypeOf(parent,AsmPartGroup):
|
|
partGroup = parent
|
|
else:
|
|
raise RuntimeError('{} is not Assembly or PartGroup'.format(
|
|
objName(parent)))
|
|
|
|
part = partGroup.getSubObject(names[0]+'.',1)
|
|
if not part:
|
|
raise RuntimeError('Invalid sub object {}, {}'.format(
|
|
objName(parent), subnameRef))
|
|
|
|
# For storing the shape of the element with proper transformation
|
|
shape = None
|
|
# For storing the placement of the movable part
|
|
pla = None
|
|
# For storing the actual geometry object of the part, in case 'part' is
|
|
# a link
|
|
obj = None
|
|
|
|
if not isTypeOf(part,Assembly,True):
|
|
getter = getattr(part.getLinkedObject(True),
|
|
'getLinkExtProperty',None)
|
|
|
|
# special treatment of link array (i.e. when ElementCount!=0), we
|
|
# allow the array element to be moveable by the solver
|
|
if getter and getter('ElementCount'):
|
|
|
|
# store both the part (i.e. the link array), and the array
|
|
# element object
|
|
part = (part,part.getSubObject(names[1]+'.',1))
|
|
|
|
# trim the subname to be after the array element
|
|
sub = '.'.join(names[2:])
|
|
|
|
# There are two states of an link array.
|
|
if getter('ElementList'):
|
|
# a) The elements are expanded as individual objects, i.e
|
|
# when ElementList has members, then the moveable Placement
|
|
# is a property of the array element. So we obtain the shape
|
|
# before 'Placement' by setting 'transform' set to False.
|
|
shape=part[1].getSubObject(sub,transform=False)
|
|
pla = part[1].Placement
|
|
obj = part[0].getLinkedObject(False)
|
|
partName = part[1].Name
|
|
part = part[1]
|
|
else:
|
|
# b) The elements are collapsed. Then the moveable Placement
|
|
# is stored inside link object's PlacementList property. So,
|
|
# the shape obtained below is already before 'Placement',
|
|
# i.e. no need to set 'transform' to False.
|
|
shape=part[1].getSubObject(sub)
|
|
obj = part[1]
|
|
try:
|
|
idx = names[1].split('_i')[-1]
|
|
# we store the array index instead, in order to modified
|
|
# Placement later when the solver is done. Also because
|
|
# that when the elements are collapsed, there is really
|
|
# no element object here.
|
|
part = (part[0],int(idx),part[1])
|
|
pla = part[0].PlacementList[idx]
|
|
except ValueError:
|
|
raise RuntimeError('invalid array subname of element {}: '
|
|
'{}'.format(objName(parent),subnameRef))
|
|
|
|
partName = '{}.{}.'.format(part[0].Name,idx)
|
|
|
|
subname = sub
|
|
|
|
if not shape:
|
|
# Here means, either the 'part' is an assembly or it is a non array
|
|
# object. We trim the subname reference to be relative to the part
|
|
# object. And obtain the shape before part's Placement by setting
|
|
# 'transform' to False
|
|
subname = '.'.join(names[1:])
|
|
shape = utils.getElementShape((part,subname),Part.Shape)
|
|
if not shape:
|
|
raise RuntimeError('cannot get geometry element from {}.{}'.format(
|
|
part.Name,subname))
|
|
pla = part.Placement
|
|
obj = part.getLinkedObject(False)
|
|
partName = part.Name
|
|
|
|
return ElementInfo(Parent = parent,
|
|
SubnameRef = subnameRef,
|
|
Part = part,
|
|
PartName = partName,
|
|
Placement = pla.copy(),
|
|
Object = obj,
|
|
Subname = subname,
|
|
Shape = shape.copy())
|
|
|
|
|
|
class AsmElementLink(AsmBase):
|
|
def __init__(self,parent):
|
|
super(AsmElementLink,self).__init__()
|
|
self.info = None
|
|
self.parent = getProxy(parent,AsmConstraint)
|
|
|
|
def linkSetup(self,obj):
|
|
super(AsmElementLink,self).linkSetup(obj)
|
|
obj.configLinkProperty('LinkedObject')
|
|
# obj.setPropertyStatus('LinkedObject','Immutable')
|
|
obj.setPropertyStatus('LinkedObject','ReadOnly')
|
|
|
|
def attach(self,obj):
|
|
obj.addProperty("App::PropertyXLink","LinkedObject"," Link",'')
|
|
super(AsmElementLink,self).attach(obj)
|
|
|
|
def canLinkProperties(self,_obj):
|
|
return False
|
|
|
|
def execute(self,_obj):
|
|
self.getInfo(True)
|
|
return False
|
|
|
|
def onChanged(self,_obj,prop):
|
|
if prop=='LinkedObject' and \
|
|
getattr(self,'parent',None) and \
|
|
not Constraint.isDisabled(self.parent.Object):
|
|
Assembly.autoSolve()
|
|
|
|
def getAssembly(self):
|
|
return self.parent.parent.parent
|
|
|
|
def getElementSubname(self):
|
|
'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 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
|
|
|
|
linked = self.Object.getLinkedObject(False)
|
|
if not linked or linked == self.Object:
|
|
raise RuntimeError('Element link broken')
|
|
element = getProxy(linked,AsmElement)
|
|
assembly = element.getAssembly()
|
|
if assembly == self.getAssembly():
|
|
return element.getElementSubname()
|
|
|
|
# The reference 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.
|
|
ref = self.Object.LinkedObject[1]
|
|
prefix = ref[0:ref.rfind('.',0,ref.rfind('.',0,-1))]
|
|
return '{}.{}.{}'.format(prefix, assembly.getPartGroup().Name,
|
|
element.getElementSubname())
|
|
|
|
def setLink(self,owner,subname,checkOnly=False):
|
|
# check if there is any sub assembly in the reference
|
|
ret = Assembly.find(owner,subname)
|
|
if not ret:
|
|
# if not, add/get an element in our own element group
|
|
sel = AsmElement.Selection(Element=None, Group=owner,
|
|
Subname=subname)
|
|
element = AsmElement.make(sel)
|
|
owner = element.Proxy.parent.Object
|
|
subname = '${}.'.format(element.Label)
|
|
else:
|
|
# if so, add/get an element from the sub assembly
|
|
sel = AsmElement.Selection(Element=None, Group=ret.Object,
|
|
Subname=ret.Subname)
|
|
element = AsmElement.make(sel)
|
|
owner = owner.Proxy.getAssembly().getPartGroup()
|
|
|
|
# This give us reference to child assembly's immediate child
|
|
# without trailing dot.
|
|
prefix = subname[:len(subname)-len(ret.Subname)-1]
|
|
|
|
# Pop the immediate child name, and replace it with child
|
|
# assembly's element group name
|
|
prefix = prefix[:prefix.rfind('.')+1] + \
|
|
resolveAssembly(ret.Assembly).getElementGroup().Name
|
|
|
|
subname = '{}.${}.'.format(prefix, element.Label)
|
|
|
|
for sibling in self.parent.Object.Group:
|
|
if sibling == self.Object:
|
|
continue
|
|
linked = sibling.LinkedObject
|
|
if isinstance(linked,tuple) and \
|
|
linked[0]==owner and linked[1]==subname:
|
|
raise RuntimeError('duplicate element link {} in constraint '
|
|
'{}'.format(objName(sibling),objName(self.parent.Object)))
|
|
if not checkOnly:
|
|
self.Object.setLink(owner,subname)
|
|
|
|
def getInfo(self,refresh=False):
|
|
if not refresh:
|
|
ret = getattr(self,'info',None)
|
|
if ret:
|
|
return ret
|
|
self.info = None
|
|
if not getattr(self,'Object',None):
|
|
return
|
|
self.info = getElementInfo(self.getAssembly().getPartGroup(),
|
|
self.getElementSubname())
|
|
return self.info
|
|
|
|
@staticmethod
|
|
def setPlacement(part,pla):
|
|
'''
|
|
called by solver after solving to adjust the placement.
|
|
|
|
part: obtained by AsmConstraint.getInfo().Part
|
|
pla: the new placement
|
|
'''
|
|
if isinstance(part,tuple):
|
|
if isinstance(part[1],int):
|
|
part[0].PlacementList = {part[1]:pla}
|
|
else:
|
|
part[1].Placement = pla
|
|
else:
|
|
part.Placement = pla
|
|
|
|
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 not gui.AsmCmdManager.AutoElementVis:
|
|
info.Constraint.setElementVisible(link.Name,False)
|
|
return link
|
|
|
|
|
|
def setPlacement(part,pla):
|
|
AsmElementLink.setPlacement(part,pla)
|
|
|
|
|
|
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)
|
|
if gui.AsmCmdManager.AutoElementVis:
|
|
vobj.OnTopWhenSelected = 2
|
|
|
|
def getDefaultColor(self):
|
|
return (1.0,60.0/255.0,60.0/255.0)
|
|
|
|
def doubleClicked(self,_vobj):
|
|
from . import mover
|
|
return mover.movePart()
|
|
|
|
def canDropObjectEx(self,_obj,owner,subname):
|
|
if logger.catchTrace('Cannot drop to AsmLink {}'.format(
|
|
objName(self.ViewObject.Object)),
|
|
self.ViewObject.Object.Proxy.setLink,
|
|
owner, subname, True):
|
|
return True
|
|
return False
|
|
|
|
def dropObjectEx(self,vobj,_obj,owner,subname):
|
|
vobj.Object.Proxy.setLink(owner,subname)
|
|
|
|
|
|
class AsmConstraint(AsmGroup):
|
|
|
|
def __init__(self,parent):
|
|
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
|
|
raise RuntimeError('Constraint type "{}" is not supported by '
|
|
'solver "{}"'.format(Constraint.getTypeName(obj),
|
|
System.getTypeName(assembly)))
|
|
|
|
def onChanged(self,obj,prop):
|
|
if prop not in _IgnoredProperties:
|
|
Constraint.onChanged(obj,prop)
|
|
Assembly.autoSolve()
|
|
|
|
def linkSetup(self,obj):
|
|
self.elements = None
|
|
super(AsmConstraint,self).linkSetup(obj)
|
|
group = obj.Group
|
|
for o in group:
|
|
getProxy(o,AsmElementLink).parent = self
|
|
if gui.AsmCmdManager.AutoElementVis:
|
|
obj.setPropertyStatus('VisibilityList','-Immutable')
|
|
obj.VisibilityList = [False]*len(group)
|
|
obj.setPropertyStatus('VisibilityList','Immutable')
|
|
Constraint.attach(obj)
|
|
obj.recompute()
|
|
|
|
def execute(self,_obj):
|
|
if not getattr(self,'_initializing',False) and\
|
|
getattr(self,'parent',None):
|
|
self.checkSupport()
|
|
self.getElements(True)
|
|
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 = []
|
|
for o in obj.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
|
|
|
|
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
|
|
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))
|
|
|
|
if not assembly:
|
|
assembly = ret[0].Assembly
|
|
selSubname = sub[:-len(ret[0].Subname)]
|
|
found = ret[0]
|
|
else:
|
|
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):
|
|
continue
|
|
|
|
# check if the selection is a constraint group or a constraint
|
|
if isTypeOf(sobj,Assembly,True) or \
|
|
isTypeOf(sobj,(AsmConstraintGroup,AsmConstraint)):
|
|
if isTypeOf(sobj,AsmConstraint):
|
|
if cstr:
|
|
raise RuntimeError('more than one constraint selected')
|
|
assembly = ret[-1].Assembly
|
|
selSubname = sub[:-len(ret[-1].Subname)]
|
|
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):
|
|
if cstr:
|
|
typeid = Constraint.getTypeID(cstr)
|
|
check = []
|
|
for o in cstr.Group:
|
|
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)
|
|
if sel.Constraint:
|
|
if undo:
|
|
FreeCAD.setActiveTransaction('Assembly change constraint')
|
|
cstr = sel.Constraint
|
|
else:
|
|
if undo:
|
|
FreeCAD.setActiveTransaction('Assembly create constraint')
|
|
constraints = sel.Assembly.Proxy.getConstraintGroup()
|
|
cstr = constraints.Document.addObject("App::FeaturePython",
|
|
name,AsmConstraint(constraints),None,True)
|
|
proxy = ViewProviderAsmConstraint(cstr.ViewObject)
|
|
logger.debug('cstr viewobject {},{},{},{}'.format(
|
|
id(proxy),id(cstr.ViewObject.Proxy),
|
|
id(proxy.ViewObject),id(cstr.ViewObject)))
|
|
constraints.setLink({-1:cstr})
|
|
Constraint.setTypeID(cstr,typeid)
|
|
|
|
try:
|
|
for e in sel.Elements:
|
|
AsmElementLink.make(AsmElementLink.MakeInfo(cstr,*e))
|
|
cstr.Proxy._initializing = False
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction()
|
|
|
|
if sel.SelObject:
|
|
FreeCADGui.Selection.clearSelection()
|
|
subname = sel.SelSubname
|
|
if subname:
|
|
subname += '.'
|
|
subname += sel.Assembly.Proxy.getConstraintGroup().Name + \
|
|
'.' + cstr.Name + '.'
|
|
FreeCADGui.Selection.addSelection(sel.SelObject,subname)
|
|
FreeCADGui.runCommand('Std_TreeSelection')
|
|
return cstr
|
|
|
|
except Exception:
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
|
|
|
|
class ViewProviderAsmConstraint(ViewProviderAsmGroup):
|
|
def attach(self,vobj):
|
|
super(ViewProviderAsmConstraint,self).attach(vobj)
|
|
if gui.AsmCmdManager.AutoElementVis:
|
|
vobj.OnTopWhenSelected = 2
|
|
|
|
def getIcon(self):
|
|
return Constraint.getIcon(self.ViewObject.Object)
|
|
|
|
def _getSelection(self,owner,subname):
|
|
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')
|
|
subname = owner.Name + '.' + subname
|
|
obj = self.ViewObject.Object
|
|
mysub = parent.getConstraintGroup().Name + '.' + obj.Name + '.'
|
|
sel = [Selection(Object=parent.Object,SubElementNames=[subname,mysub])]
|
|
typeid = Constraint.getTypeID(obj)
|
|
return AsmConstraint.getSelection(typeid,sel)
|
|
|
|
def canDropObjectEx(self,_obj,owner,subname):
|
|
cstr = self.ViewObject.Object
|
|
if logger.catchTrace('Cannot drop to AsmConstraint {}'.format(cstr),
|
|
self._getSelection,owner,subname):
|
|
return True
|
|
return False
|
|
|
|
def dropObjectEx(self,_vobj,_obj,owner,subname):
|
|
sel = self._getSelection(owner,subname)
|
|
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)
|
|
|
|
|
|
class AsmConstraintGroup(AsmGroup):
|
|
def __init__(self,parent):
|
|
self.parent = getProxy(parent,Assembly)
|
|
super(AsmConstraintGroup,self).__init__()
|
|
|
|
def getAssembly(self):
|
|
return self.parent
|
|
|
|
def linkSetup(self,obj):
|
|
super(AsmConstraintGroup,self).linkSetup(obj)
|
|
for o in obj.Group:
|
|
cstr = getProxy(o,AsmConstraint)
|
|
if cstr:
|
|
cstr.parent = self
|
|
obj.recompute()
|
|
|
|
@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 canDropObjects(self):
|
|
return False
|
|
|
|
|
|
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)
|
|
for o in obj.Group:
|
|
getProxy(o,AsmElement).parent = self
|
|
obj.cacheChildLabel()
|
|
|
|
def getAssembly(self):
|
|
return self.parent
|
|
|
|
def onChildLabelChange(self,obj,label):
|
|
names = set()
|
|
for o in self.Object.Group:
|
|
if o != obj:
|
|
names.add(o.Name)
|
|
if label not in names:
|
|
return
|
|
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
|
|
|
|
@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 onDelete(self,_obj,_subs):
|
|
return False
|
|
|
|
def canDropObjectEx(self,_obj,owner,subname):
|
|
if not subname:
|
|
return False
|
|
proxy = self.ViewObject.Object.Proxy
|
|
return proxy.getAssembly().getPartGroup()==owner
|
|
|
|
def dropObjectEx(self,_vobj,_obj,owner,subname):
|
|
AsmElement.make(AsmElement.Selection(
|
|
Element=None, Group=owner, Subname=subname))
|
|
|
|
|
|
BuildShapeNone = 'None'
|
|
BuildShapeCompound = 'Compound'
|
|
BuildShapeFuse = 'Fuse'
|
|
BuildShapeCut = 'Cut'
|
|
BuildShapeNames = (BuildShapeNone,BuildShapeCompound,
|
|
BuildShapeFuse,BuildShapeCut)
|
|
|
|
class Assembly(AsmGroup):
|
|
_Timer = QtCore.QTimer()
|
|
_PartMap = {} # maps part to assembly
|
|
_PartArrayMap = {} # maps array part to assembly
|
|
|
|
def __init__(self):
|
|
self.parts = set()
|
|
self.partArrays = set()
|
|
self.constraints = None
|
|
super(Assembly,self).__init__()
|
|
|
|
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]
|
|
return newParts
|
|
|
|
def execute(self,obj):
|
|
self.constraints = None
|
|
self.buildShape()
|
|
System.touch(obj)
|
|
obj.ViewObject.Proxy.onExecute()
|
|
|
|
parts = set()
|
|
partArrays = set()
|
|
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])
|
|
else:
|
|
parts.add(info.Part)
|
|
parts = self._collectParts(self.parts,parts,Assembly._PartMap)
|
|
partArrays = self._collectParts(
|
|
self.partArrays,partArrays,Assembly._PartArrayMap)
|
|
|
|
return False # return False to call LinkBaseExtension::execute()
|
|
|
|
@classmethod
|
|
def canAutoSolve(cls):
|
|
from . import solver
|
|
return gui.AsmCmdManager.AutoRecompute and \
|
|
not FreeCADGui.ActiveDocument.Transacting and \
|
|
not FreeCAD.isRestoring() and \
|
|
not solver.isBusy() and \
|
|
not ViewProviderAssembly.isBusy()
|
|
|
|
@classmethod
|
|
def checkPartChange(cls, obj, prop):
|
|
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(True)
|
|
|
|
@classmethod
|
|
def autoSolve(cls,force=False):
|
|
if force or cls.canAutoSolve():
|
|
if not cls._Timer.isSingleShot():
|
|
cls._Timer.setSingleShot(True)
|
|
cls._Timer.timeout.connect(Assembly.onSolverTimer)
|
|
logger.debug('auto solve scheduled',frame=1)
|
|
cls._Timer.start(300)
|
|
|
|
@classmethod
|
|
def cancelAutoSolve(cls):
|
|
cls._Timer.stop()
|
|
|
|
@classmethod
|
|
def onSolverTimer(cls):
|
|
if cls.canAutoSolve():
|
|
from . import solver
|
|
logger.catch('solver exception when auto recompute',
|
|
solver.solve, FreeCAD.ActiveDocument.Objects, True)
|
|
|
|
def onSolverChanged(self,setup=False):
|
|
for obj in self.getConstraintGroup().Group:
|
|
# 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 = not setup or 'Touched' in obj.State
|
|
obj.recompute()
|
|
if not touched:
|
|
obj.purgeTouched()
|
|
|
|
def buildShape(self):
|
|
obj = self.Object
|
|
if obj.BuildShape == BuildShapeNone:
|
|
if not obj.Shape.isNull():
|
|
obj.Shape = Part.Shape()
|
|
return
|
|
|
|
shape = []
|
|
partGroup = self.getPartGroup(obj)
|
|
group = partGroup.Group
|
|
if not group:
|
|
raise RuntimeError('no parts')
|
|
if obj.BuildShape == BuildShapeCut:
|
|
shape = Part.getShape(group[0]).Solids
|
|
if not shape:
|
|
raise RuntimeError('First part has no solid')
|
|
if len(shape)>1:
|
|
shape = [shape[0].fuse(shape[1:])]
|
|
group = group[1:]
|
|
|
|
for o in group:
|
|
if obj.isElementVisible(o.Name):
|
|
shape += Part.getShape(o).Solids
|
|
if not shape:
|
|
raise RuntimeError('No solids found in parts')
|
|
if len(shape) == 1:
|
|
obj.Shape = shape[0]
|
|
elif obj.BuildShape == BuildShapeFuse:
|
|
obj.Shape = shape[0].fuse(shape[1:])
|
|
elif obj.BuildShape == BuildShapeCut:
|
|
if len(shape)>2:
|
|
obj.Shape = shape[0].cut(shape[1].fuse(shape[2:]))
|
|
else:
|
|
obj.Shape = shape[0].cut(shape[1])
|
|
else:
|
|
obj.Shape = Part.makeCompound(shape)
|
|
|
|
def attach(self, obj):
|
|
obj.addProperty("App::PropertyEnumeration","BuildShape","Base",'')
|
|
obj.BuildShape = BuildShapeNames
|
|
super(Assembly,self).attach(obj)
|
|
|
|
def linkSetup(self,obj):
|
|
self.parts = set()
|
|
self.partArrays = set()
|
|
obj.configLinkProperty('Placement')
|
|
super(Assembly,self).linkSetup(obj)
|
|
System.attach(obj)
|
|
self.onChanged(obj,'BuildShape')
|
|
|
|
# make sure all children are there, first constraint group, then element
|
|
# group, and finally part group. Call getPartGroup below will make sure
|
|
# all groups exist. The order of the group is important to make sure
|
|
# correct rendering and picking behavior
|
|
self.getPartGroup(True)
|
|
|
|
self.onSolverChanged(True)
|
|
|
|
def onChanged(self, obj, prop):
|
|
if prop == 'BuildShape':
|
|
if not obj.BuildShape or obj.BuildShape == BuildShapeCompound:
|
|
obj.setPropertyStatus('Shape','-Transient')
|
|
else:
|
|
obj.setPropertyStatus('Shape','Transient')
|
|
return
|
|
if prop not in _IgnoredProperties:
|
|
System.onChanged(obj,prop)
|
|
Assembly.autoSolve()
|
|
|
|
def getConstraintGroup(self, create=False):
|
|
obj = self.Object
|
|
try:
|
|
ret = obj.Group[0]
|
|
checkType(ret,AsmConstraintGroup)
|
|
parent = getattr(ret.Proxy,'parent',None)
|
|
if not parent:
|
|
ret.Proxy.parent = self
|
|
elif parent!=self:
|
|
raise RuntimeError('invalid parent of constraint group '
|
|
'{}'.format(objName(ret)))
|
|
return ret
|
|
except IndexError:
|
|
if not create 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 = None
|
|
cstrGroup = self.getConstraintGroup()
|
|
if not cstrGroup:
|
|
return
|
|
ret = []
|
|
for o in cstrGroup.Group:
|
|
checkType(o,AsmConstraint)
|
|
if Constraint.isDisabled(o):
|
|
logger.debug('skip constraint {}'.format(cstrName(o)))
|
|
continue
|
|
if not System.isConstraintSupported(self.Object,
|
|
Constraint.getTypeName(o)):
|
|
logger.debug('skip unsupported constraint '
|
|
'{}'.format(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
|
|
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
|
|
|
|
@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)
|
|
ViewProviderAssembly(obj.ViewObject)
|
|
obj.Visibility = True
|
|
obj.purgeTouched()
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction()
|
|
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: '.' separted sub-object reference, or string list of sub-object
|
|
names. Must contain no sub element name.
|
|
|
|
childType: optional checking of the child type.
|
|
|
|
recursive: If True, continue finding the child of the next assembly.
|
|
|
|
relativeToChild: If True, the returned subname is realtive to the child
|
|
object found, or else, it is relative to the assembly, i.e., including
|
|
the child's name
|
|
|
|
Return None if not found, or (assembly,child,sub), where 'sub' is the
|
|
remaining sub name list. If recursive is True, then return a list of
|
|
tuples
|
|
'''
|
|
assembly = None
|
|
child = None
|
|
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)
|
|
|
|
|
|
class ViewProviderAssembly(ViewProviderAsmGroup):
|
|
def __init__(self,vobj):
|
|
self._movingPart = None
|
|
super(ViewProviderAssembly,self).__init__(vobj)
|
|
|
|
def _convertSubname(self,owner,subname):
|
|
sub = subname.split('.')
|
|
if not sub:
|
|
return
|
|
me = self.ViewObject.Object
|
|
partGroup = me.Proxy.getPartGroup().ViewObject
|
|
if sub == me.Name:
|
|
return partGroup,partGroup,subname[len[sub]+1:]
|
|
return partGroup,owner,subname
|
|
|
|
def canDropObjectEx(self,obj,owner,subname):
|
|
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):
|
|
info = self._convertSubname(owner,subname)
|
|
if not info:
|
|
return False
|
|
partGroup,owner,subname = info
|
|
partGroup.dropObject(obj,owner,subname)
|
|
|
|
def getIcon(self):
|
|
return System.getIcon(self.ViewObject.Object)
|
|
|
|
def doubleClicked(self, _vobj):
|
|
from . import mover
|
|
return mover.movePart()
|
|
|
|
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
|
|
return (FreeCADGui.editDocument().EditingTransform,
|
|
self._movingPart.draggerPlacement,
|
|
self._movingPart.bbox)
|
|
|
|
_Busy = False
|
|
|
|
def onDragStart(self):
|
|
Assembly.cancelAutoSolve();
|
|
FreeCADGui.Selection.clearSelection()
|
|
self.__class__._Busy = True
|
|
FreeCAD.setActiveTransaction('Assembly move')
|
|
|
|
def onDragMotion(self):
|
|
return self._movingPart.move()
|
|
|
|
def onDragEnd(self):
|
|
self.__class__._Busy = False
|
|
FreeCAD.closeActiveTransaction()
|
|
|
|
def unsetEdit(self,_vobj,_mode):
|
|
self._movingPart = None
|
|
return False
|
|
|
|
@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.Length = 10
|
|
obj.Width = 10
|
|
obj.Proxy = self
|
|
|
|
def execute(self,obj):
|
|
if not obj.Length or not obj.Width:
|
|
raise RuntimeError('invalid workplane size')
|
|
obj.Shape = Part.makePlane(obj.Length,obj.Width)
|
|
|
|
def __getstate__(self):
|
|
return
|
|
|
|
def __setstate__(self,_state):
|
|
return
|
|
|
|
Info = namedtuple('AsmWorkPlaneSelectionInfo',
|
|
('SelObj','SelSubname','PartGroup'))
|
|
|
|
@staticmethod
|
|
def getSelection(sels=None):
|
|
if not sels:
|
|
sels = FreeCADGui.Selection.getSelectionEx('',False)
|
|
if not sels:
|
|
raise RuntimeError('no selection')
|
|
if len(sels)!=1 or len(sels[0].SubElementNames)>1:
|
|
raise RuntimeError('too many selections')
|
|
if sels[0].SubElementNames:
|
|
sub = sels[0].SubElementNames[0]
|
|
else:
|
|
sub = ''
|
|
ret = Assembly.find(sels[0].Object,sub,
|
|
relativeToChild=False,keepEmptyChild=True)
|
|
if not ret:
|
|
raise RuntimeError('invalid selection')
|
|
if ret.Subname:
|
|
sub = sub[:-len(ret.Subname)]
|
|
return AsmWorkPlane.Info(
|
|
SelObj = sels[0].Object,
|
|
SelSubname = sub,
|
|
PartGroup = ret.Assembly.Proxy.getPartGroup())
|
|
|
|
@staticmethod
|
|
def make(sels=None,name='Workplane', undo=True):
|
|
info = AsmWorkPlane.getSelection(sels)
|
|
doc = info.PartGroup.Document
|
|
if undo:
|
|
FreeCAD.setActiveTransaction('Assembly create workplane')
|
|
try:
|
|
obj = doc.addObject('Part::FeaturePython',name)
|
|
AsmWorkPlane(obj)
|
|
ViewProviderAsmWorkPlane(obj.ViewObject)
|
|
bbox = info.PartGroup.ViewObject.getBoundingBox()
|
|
if bbox.isValid():
|
|
obj.Length = bbox.DiagonalLength*0.5
|
|
obj.Width = obj.Length
|
|
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')
|
|
return obj
|
|
except Exception:
|
|
if undo:
|
|
FreeCAD.closeActiveTransaction(True)
|
|
raise
|
|
|
|
|
|
class ViewProviderAsmWorkPlane(ViewProviderAsmBase):
|
|
_iconName = 'Assembly_Workplane.svg'
|
|
|
|
def __init__(self,vobj):
|
|
vobj.Transparency = 50
|
|
vobj.LineColor = (0.0,0.33,1.0,1.0)
|
|
super(ViewProviderAsmWorkPlane,self).__init__(vobj)
|
|
|
|
def canDropObjects(self):
|
|
return False
|
|
|
|
def getDisplayModes(self, _vobj):
|
|
modes=[]
|
|
return modes
|
|
|
|
def setDisplayMode(self, mode):
|
|
return mode
|
|
|