FreeCAD_assembly3/freecad/asm3/assembly.py
2020-11-16 20:50:40 +08:00

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)