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' if isTypeOf(obj,(AsmConstraintGroup,AsmConstraint)): # the order inside constraint group actually matters, so do not # engage no touch parent = None block = False notouch = False else: 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: 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 canReplaceObject(self, _old, _new): return False def replaceObject(self,_old,_new): 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 def canReplaceObject(self, _oldObj, newObj): return newObj in self.ViewObject.Object.Group def replaceObject(self, oldObj, newObj): try: children = self.ViewObject.Object.Group old_idx = children.index(oldObj) new_idx = children.index(newObj) del children[new_idx] children.insert(old_idx, newObj) editGroup(self.ViewObject.Object, children) return True except Exception: 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 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 canReplaceObject(self, _old, _new): return True 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) title = 'Flip element' if flipElement else 'Flip part' FreeCAD.setActiveTransaction(title) try: if flipElement: obj.Offset = rot.multiply(obj.Offset) else: if hasProperty(obj,'Count'): # for multiplied elements, we shall flip the first part of # the first pairing elements. Note that constraint # multiplication algorithm will sort the element pairs based # on their proximity to stablize index change info = obj.Proxy.getInfo(expand=True)[0] shape = Part.getShape(obj, '%d.' % info.Part[1], transform=False) offset = utils.getElementPlacement(shape) else: offset = utils.getElementPlacement(obj.getSubObject('')) offset = offset.multiply(rot).multiply(offset.inverse()) pla = offset.multiply(info.Placement) setPlacement(info.Part, pla) obj.recompute(True) FreeCAD.closeActiveTransaction() except Exception: QtGui.QMessageBox.critical(None, 'Flip', title + ' failed') 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 Exception: 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 = objName(part) 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 getLinkedElement(self): 'Get linked AsmElement object' 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') return linked 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(): pla = 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()) 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): # adjust placement calculated based on obj.Offset if not utils.isSamePlacement(obj.Placement,pla): obj.Placement = pla 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 = [] plaList = [] # We change this AsmElementLink into a LinkArray to visually display # the multipled element (i.e. the first element in the parent # constraint). Because of this, we shall encode the # AsmElementLink.Offset of the element into each individual # placement in AsmElementLink.PlacementList. So reset # AsmElementLink.Placement here first, and then add the extra offset # 'pla'. obj.Placement = FreeCAD.Placement() offset = info.Placement.inverse() * pla 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 # adjust placement calculated based on obj.Offset if not utils.isSamePlacement(obj.Placement,pla): obj.Placement = pla 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=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 properly 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) # adjust the first element to point to the first array # element. 'elements[0]' is an AsmElementLink, so follow its # link first to obtain the AsmElement, and then change its # subname reference element = elements[0].Proxy.getLinkedElement() link = element.LinkedObject if isinstance(link, tuple): element.setLink(link[0], '0.' + link[1]) except Exception: logger.error(traceback.format_exc()) 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): 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) # AsmRelationGroup used to not having the LinkBaseExtension for # the sake of simplicity. It is added now to make it a LinkGroup so that # its children can be auto deleted by setting GroupMode to 1 if not obj.hasExtension('App::LinkBaseExtensionPython'): obj.addExtension('App::LinkBaseExtensionPython', None) obj.addProperty("App::PropertyEnumeration","GroupMode","Base",'') obj.configLinkProperty(ElementList='Group', LinkMode='GroupMode') obj.GroupMode = 1 # auto delete children obj.setPropertyStatus('GroupMode', ('Hidden','Immutable','Transient')) else: obj.configLinkProperty(ElementList='Group', LinkMode='GroupMode') 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 canDelete(self,_obj): return True def claimChildren(self): return self.ViewObject.Object.Group def getDetailPath(self,subname,path,append): vobj = self.ViewObject idx = subname.find('.') if idx > 0: obj = vobj.Object sobj = obj.getSubObject(subname[:idx+1], retType=1) # checking of relation of part that is a link array element if sobj != obj: if isTypeOf(sobj, AsmRelation): subname = str(sobj.Index) + subname[idx:] else: subname = '' return vobj.getDetailPath(subname,path,append) 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 canReplaceObject(self, _old, _new): return False def replaceObject(self,_old,_new): return False 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(1,0,0),90)) elif tp==2: pla = FreeCAD.Placement(info.Placement.Base, FreeCAD.Rotation(FreeCAD.Vector(0,1,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)