Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bc2d5d611f | ||
![]() |
e785510c68 | ||
![]() |
b9c7af40fd | ||
![]() |
b590b57793 | ||
![]() |
6c68f0f901 | ||
![]() |
caad48e927 | ||
![]() |
70d161f336 | ||
![]() |
c6f6c481d2 | ||
![]() |
fe6a792202 | ||
![]() |
da050987f5 | ||
![]() |
6fbded4460 | ||
![]() |
6bb16b5550 | ||
![]() |
c3380e602d | ||
![]() |
9f4fa40b8f | ||
![]() |
4edc0fbca6 | ||
![]() |
490da82590 | ||
![]() |
e44f47073b | ||
![]() |
0c1f659fde | ||
![]() |
5da0bd5554 | ||
![]() |
14dffc7b28 | ||
![]() |
652ea7af6c | ||
![]() |
c8cf0a1e32 | ||
![]() |
19163afe88 | ||
![]() |
0acaa5e9c3 | ||
![]() |
9e34ecd1c0 | ||
![]() |
3bd99b64d2 | ||
![]() |
9f904ede3f | ||
![]() |
72f64e9ed5 | ||
![]() |
91c34b2fcb | ||
![]() |
1cdf1fe5c4 | ||
![]() |
edac36d4df |
|
@ -102,26 +102,28 @@ def editGroup(obj,children,notouch=None):
|
|||
change = '-Immutable'
|
||||
revert = 'Immutable'
|
||||
|
||||
parent = getattr(obj,'_Parent',None)
|
||||
if parent and 'Touched' in parent.State:
|
||||
if isTypeOf(obj,(AsmConstraintGroup,AsmConstraint)):
|
||||
# the order inside constraint group actually matters, so do not
|
||||
# engage no touch
|
||||
parent = None
|
||||
|
||||
if not hasProperty(obj,'NoTouch'):
|
||||
block = False
|
||||
notouch = False
|
||||
elif notouch is None:
|
||||
if (isTypeOf(parent,AsmConstraintGroup) or \
|
||||
isTypeOf(obj,AsmConstraintGroup)):
|
||||
# the order inside constraint group actually matters, so do not
|
||||
# engage no touch
|
||||
else:
|
||||
parent = getattr(obj,'_Parent',None)
|
||||
if parent and 'Touched' in parent.State:
|
||||
parent = None
|
||||
else:
|
||||
|
||||
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
|
||||
if notouch:
|
||||
obj.NoTouch = True
|
||||
block = gui.AsmCmdManager.AutoRecompute
|
||||
if block:
|
||||
gui.AsmCmdManager.AutoRecompute = False
|
||||
|
||||
try:
|
||||
if change:
|
||||
obj.setPropertyStatus('Group',change)
|
||||
|
@ -137,10 +139,10 @@ def editGroup(obj,children,notouch=None):
|
|||
parent.purgeTouched()
|
||||
|
||||
def setupSortMenu(menu,func,func2):
|
||||
action = QtGui.QAction(QtGui.QIcon(),"Sort A~Z",menu)
|
||||
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 Z~A",menu)
|
||||
action = QtGui.QAction(QtGui.QIcon(),"Sort element Z~A",menu)
|
||||
QtCore.QObject.connect(
|
||||
action,QtCore.SIGNAL("triggered()"),func2)
|
||||
menu.addAction(action)
|
||||
|
@ -217,7 +219,10 @@ class ViewProviderAsmBase(object):
|
|||
vobj.Proxy = self
|
||||
self.attach(vobj)
|
||||
|
||||
def replaceObject(self,_new,_old):
|
||||
def canReplaceObject(self, _old, _new):
|
||||
return False
|
||||
|
||||
def replaceObject(self,_old,_new):
|
||||
return False
|
||||
|
||||
def canAddToSceneGraph(self):
|
||||
|
@ -285,7 +290,7 @@ class AsmGroup(AsmBase):
|
|||
|
||||
class ViewProviderAsmGroup(ViewProviderAsmBase):
|
||||
def claimChildren(self):
|
||||
return self.ViewObject.Object.Group
|
||||
return getattr(self.ViewObject.Object, 'Group', [])
|
||||
|
||||
def doubleClicked(self, _vobj):
|
||||
return False
|
||||
|
@ -293,6 +298,21 @@ class ViewProviderAsmGroup(ViewProviderAsmBase):
|
|||
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):
|
||||
|
@ -309,7 +329,9 @@ class AsmPartGroup(AsmGroup):
|
|||
def getSubObjects(self,obj,_reason):
|
||||
# Deletion order problem may cause exception here. Just silence it
|
||||
try:
|
||||
return [ '{}.'.format(o.Name) for o in flattenGroup(obj) ]
|
||||
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
|
||||
|
||||
|
@ -317,6 +339,10 @@ class AsmPartGroup(AsmGroup):
|
|||
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):
|
||||
|
@ -374,7 +400,6 @@ class AsmPartGroup(AsmGroup):
|
|||
obj = parent.Document.addObject("Part::FeaturePython",name,
|
||||
AsmPartGroup(parent),None,True)
|
||||
obj.setPropertyStatus('Placement',('Output','Hidden'))
|
||||
obj.setPropertyStatus('Shape','Output')
|
||||
ViewProviderAsmPartGroup(obj.ViewObject)
|
||||
obj.purgeTouched()
|
||||
return obj
|
||||
|
@ -383,9 +408,6 @@ class AsmPartGroup(AsmGroup):
|
|||
class ViewProviderAsmPartGroup(ViewProviderAsmGroup):
|
||||
_iconName = 'Assembly_Assembly_Part_Tree.svg'
|
||||
|
||||
def replaceObject(self,new,old):
|
||||
return self.Object.replaceObject(new,old)
|
||||
|
||||
def canDropObjectEx(self,obj,_owner,_subname,_elements):
|
||||
return isTypeOf(obj,Assembly, True) or not isTypeOf(obj,AsmBase)
|
||||
|
||||
|
@ -445,6 +467,9 @@ class ViewProviderAsmPartGroup(ViewProviderAsmGroup):
|
|||
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:
|
||||
|
@ -620,12 +645,7 @@ class AsmElement(AsmBase):
|
|||
if not obj:
|
||||
return False # broken beyond fix
|
||||
|
||||
subs = Part.splitSubname(subname)
|
||||
if not subs[1]:
|
||||
return False # no mapped element name
|
||||
|
||||
shape = linked.getSubObject(subs[0])
|
||||
if not utils.getElement(shape, subs[1]):
|
||||
if not utils.getElement(linked, subname):
|
||||
return True
|
||||
|
||||
def fix(self):
|
||||
|
@ -640,7 +660,7 @@ class AsmElement(AsmBase):
|
|||
if not subs[1]:
|
||||
raise RuntimeError('No mapped sub-element found')
|
||||
|
||||
shape = linked.getSubObject(subs[0])
|
||||
shape = Part.getShape(linked,subs[0])
|
||||
if utils.getElement(shape, subs[1]):
|
||||
return
|
||||
|
||||
|
@ -685,6 +705,10 @@ class AsmElement(AsmBase):
|
|||
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
|
||||
|
@ -692,7 +716,9 @@ class AsmElement(AsmBase):
|
|||
info = None
|
||||
try:
|
||||
info = self.getInfo(False)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.warn(str(e))
|
||||
|
||||
self.updatePlacement()
|
||||
|
||||
if not gui.AsmCmdManager.AutoFixElement:
|
||||
|
@ -809,10 +835,13 @@ class AsmElement(AsmBase):
|
|||
|
||||
link = self.Object.LinkedObject
|
||||
if not isinstance(link,tuple):
|
||||
raise RuntimeError('Borken element link')
|
||||
raise RuntimeError('Broken element link')
|
||||
obj = link[0].getSubObject(link[1],1)
|
||||
if not obj:
|
||||
raise RuntimeError('Borken element link')
|
||||
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
|
||||
|
@ -1183,7 +1212,8 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop):
|
|||
if not vobj or hasProperty(vobj.Object,'Radius'):
|
||||
return
|
||||
if getattr(vobj,'ShowCS',False) or\
|
||||
gui.AsmCmdManager.ShowElementCS:
|
||||
gui.AsmCmdManager.ShowElementCS or\
|
||||
not hasattr(vobj.Object,'Shape'):
|
||||
return True
|
||||
return utils.isInfinite(vobj.Object.Shape)
|
||||
|
||||
|
@ -1356,18 +1386,29 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop):
|
|||
rot = FreeCAD.Rotation(FreeCAD.Vector(1,0,0),180)
|
||||
rot = FreeCAD.Placement(FreeCAD.Vector(), rot)
|
||||
|
||||
FreeCAD.setActiveTransaction(
|
||||
'Flip element' if flipElement else 'Flip part')
|
||||
title = 'Flip element' if flipElement else 'Flip part'
|
||||
FreeCAD.setActiveTransaction(title)
|
||||
try:
|
||||
if flipElement:
|
||||
obj.Offset = rot.multiply(obj.Offset)
|
||||
else:
|
||||
offset = utils.getElementPlacement(obj.getSubObject(''))
|
||||
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())
|
||||
setPlacement(info.Part, offset.multiply(info.Placement))
|
||||
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
|
||||
|
||||
|
@ -1379,6 +1420,20 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop):
|
|||
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)
|
||||
|
@ -1570,7 +1625,7 @@ def getElementInfo(parent,subname,
|
|||
# no element object here.
|
||||
part = (part[0],idx,part[1],True)
|
||||
pla = part[0].Placement.multiply(plaList[idx])
|
||||
except ValueError:
|
||||
except Exception:
|
||||
raise RuntimeError('invalid array subname of element '
|
||||
'{}: {}'.format(objName(parent),subnameRef))
|
||||
|
||||
|
@ -1592,7 +1647,7 @@ def getElementInfo(parent,subname,
|
|||
'{}.{}'.format(objName(part),subname))
|
||||
pla = getattr(part,'Placement',FreeCAD.Placement())
|
||||
obj = part.getLinkedObject(False)
|
||||
partName = part.Name
|
||||
partName = objName(part)
|
||||
|
||||
if transformShape:
|
||||
# Copy and transform shape. We have to copy the shape here to work
|
||||
|
@ -1648,6 +1703,11 @@ class AsmElementLink(AsmBase):
|
|||
|
||||
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):
|
||||
|
@ -1771,6 +1831,17 @@ class AsmElementLink(AsmBase):
|
|||
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'
|
||||
|
||||
|
@ -1896,8 +1967,7 @@ class AsmElementLink(AsmBase):
|
|||
info = self.info
|
||||
|
||||
if obj.Offset.isIdentity():
|
||||
if not obj.Placement.isIdentity():
|
||||
obj.Placement = FreeCAD.Placement()
|
||||
pla = FreeCAD.Placement()
|
||||
else:
|
||||
# obj.Offset is in the element shape's coordinate system, we need to
|
||||
# transform it to the assembly coordinate system
|
||||
|
@ -1905,8 +1975,6 @@ class AsmElementLink(AsmBase):
|
|||
mOffset = obj.Offset.toMatrix()
|
||||
mat = info.Placement.toMatrix()*mShape
|
||||
pla = FreeCAD.Placement(mat*mOffset*mat.inverse())
|
||||
if not utils.isSamePlacement(obj.Placement,pla):
|
||||
obj.Placement = pla
|
||||
info.Shape.transformShape(mShape*mOffset*mShape.inverse())
|
||||
|
||||
info = ElementInfo(Parent = info.Parent,
|
||||
|
@ -1922,6 +1990,9 @@ class AsmElementLink(AsmBase):
|
|||
|
||||
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
|
||||
|
@ -1933,8 +2004,18 @@ class AsmElementLink(AsmBase):
|
|||
self.infos.append(info)
|
||||
return self.infos if expand else self.info
|
||||
infos = []
|
||||
offset = info.Placement.inverse()
|
||||
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]:
|
||||
|
@ -1945,7 +2026,9 @@ class AsmElementLink(AsmBase):
|
|||
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,
|
||||
|
@ -1959,6 +2042,10 @@ class AsmElementLink(AsmBase):
|
|||
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,
|
||||
|
@ -2093,6 +2180,22 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop):
|
|||
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):
|
||||
|
||||
|
@ -2277,7 +2380,7 @@ class AsmConstraint(AsmGroup):
|
|||
# element shape
|
||||
|
||||
offset = FreeCAD.Vector(getattr(obj,'OffsetX',0),
|
||||
getattr(obj,'Offset&',0),
|
||||
getattr(obj,'OffsetY',0),
|
||||
getattr(obj,'Offset',0))
|
||||
poses = poses[:count]
|
||||
infos0 = firstChild.Proxy.getInfo(expand=True)[:count]
|
||||
|
@ -2296,7 +2399,7 @@ class AsmConstraint(AsmGroup):
|
|||
if i<len(prev) and prev[i]<count:
|
||||
j = prev[i]
|
||||
if used[i]<0 and not order[j] and \
|
||||
pos0.distanceToPoint(poses[j]) < 1e-7:
|
||||
pos0.distanceToPoint(poses[j]) < 1e-6:
|
||||
distances[i] = 0
|
||||
if not elements[i]._refPla:
|
||||
pla = infos[j].Placement.multiply(
|
||||
|
@ -2313,7 +2416,7 @@ class AsmConstraint(AsmGroup):
|
|||
if order[j]:
|
||||
continue
|
||||
d = pos0.distanceToPoint(pos)
|
||||
if used[i]<0 and d < 1e-7:
|
||||
if used[i]<0 and d < 1e-6:
|
||||
distances[i] = 0
|
||||
if not elements[i]._refPla:
|
||||
pla = infos[j].Placement.multiply(
|
||||
|
@ -2347,6 +2450,10 @@ class AsmConstraint(AsmGroup):
|
|||
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
|
||||
|
||||
|
@ -2376,14 +2483,21 @@ class AsmConstraint(AsmGroup):
|
|||
if ref:
|
||||
pla = pla.multiply(ref)
|
||||
else:
|
||||
pla = info0.Placement.multiply(pla.multiply(pla0.inverse()))
|
||||
pla = pla.multiply(p0.inverse())
|
||||
showPart(partGroup,info0.Part)
|
||||
touched = True
|
||||
setPlacement(info0.Part,pla,True)
|
||||
|
||||
if touched:
|
||||
firstChild.Proxy.getInfo(True)
|
||||
firstChild.purgeTouched()
|
||||
# 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\
|
||||
|
@ -2688,7 +2802,17 @@ class AsmConstraint(AsmGroup):
|
|||
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))
|
||||
|
||||
|
@ -2759,7 +2883,7 @@ class ViewProviderAsmConstraint(ViewProviderAsmGroup):
|
|||
def setupContextMenu(self,vobj,menu):
|
||||
obj = vobj.Object
|
||||
action = QtGui.QAction(QtGui.QIcon(),
|
||||
"Enable" if obj.Disabled else "Disable", menu)
|
||||
"Enable constraint" if obj.Disabled else "Disable constraint", menu)
|
||||
QtCore.QObject.connect(
|
||||
action,QtCore.SIGNAL("triggered()"),self.toggleDisable)
|
||||
menu.addAction(action)
|
||||
|
@ -3008,15 +3132,13 @@ class AsmRelationGroup(AsmBase):
|
|||
super(AsmRelationGroup,self).__init__()
|
||||
|
||||
def attach(self,obj):
|
||||
# AsmRelationGroup do not install LinkBaseExtension
|
||||
# obj.addExtension('App::LinkBaseExtensionPython', None)
|
||||
|
||||
obj.addProperty('App::PropertyLinkList','Group','')
|
||||
obj.setPropertyStatus('Group','Hidden')
|
||||
obj.addProperty('App::PropertyLink','Constraints','')
|
||||
# this is to make sure relations are recomputed after all constraints
|
||||
obj.Constraints = self.parent.getConstraintGroup()
|
||||
obj.setPropertyStatus('Constraints',('Hidden','Immutable'))
|
||||
|
||||
self.linkSetup(obj)
|
||||
|
||||
def getViewProviderName(self,_obj):
|
||||
|
@ -3024,6 +3146,20 @@ class AsmRelationGroup(AsmBase):
|
|||
|
||||
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:
|
||||
|
@ -3440,15 +3576,26 @@ class ViewProviderAsmRelation(ViewProviderAsmBase):
|
|||
def canDropObjects(self):
|
||||
return False
|
||||
|
||||
def onDelete(self,_vobj,_subs):
|
||||
return False
|
||||
|
||||
def canDelete(self,_obj):
|
||||
return True
|
||||
|
||||
def claimChildren(self):
|
||||
return self.ViewObject.Object.Group
|
||||
|
||||
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'
|
||||
|
@ -4203,11 +4350,12 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
|
|||
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" if obj.Freeze else "Freeze", menu)
|
||||
"Unfreeze assembly" if obj.Freeze else "Freeze assembly", menu)
|
||||
QtCore.QObject.connect(
|
||||
action,QtCore.SIGNAL("triggered()"),self.toggleFreeze)
|
||||
menu.addAction(action)
|
||||
|
@ -4218,6 +4366,7 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
|
|||
'Unfreeze assembly' if obj.Freeze else 'Freeze assembly')
|
||||
try:
|
||||
obj.Freeze = not obj.Freeze
|
||||
obj.recompute(True)
|
||||
FreeCAD.closeActiveTransaction()
|
||||
except Exception:
|
||||
FreeCAD.closeActiveTransaction(True)
|
||||
|
@ -4237,6 +4386,12 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
|
|||
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:
|
||||
|
@ -4345,6 +4500,9 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
|
|||
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()
|
||||
|
@ -4365,10 +4523,7 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
|
|||
self.showParts()
|
||||
|
||||
def finishRestoring(self):
|
||||
if not hasProperty(self.ViewObject,'ShowParts'):
|
||||
self.ViewObject.addProperty("App::PropertyBool","ShowParts"," Link")
|
||||
else:
|
||||
self.showParts()
|
||||
self.showParts()
|
||||
|
||||
@classmethod
|
||||
def isBusy(cls):
|
||||
|
@ -4489,10 +4644,10 @@ class AsmWorkPlane(object):
|
|||
else:
|
||||
if tp==1:
|
||||
pla = FreeCAD.Placement(info.Placement.Base,
|
||||
FreeCAD.Rotation(FreeCAD.Vector(0,1,0),-90))
|
||||
FreeCAD.Rotation(FreeCAD.Vector(1,0,0),90))
|
||||
elif tp==2:
|
||||
pla = FreeCAD.Placement(info.Placement.Base,
|
||||
FreeCAD.Rotation(FreeCAD.Vector(1,0,0),90))
|
||||
FreeCAD.Rotation(FreeCAD.Vector(0,1,0),-90))
|
||||
else:
|
||||
pla = info.Placement
|
||||
|
||||
|
|
|
@ -245,11 +245,15 @@ def _lw(solver,partInfo,subname,shape,retAll=False):
|
|||
'return a handle for either a line or a plane depending on the shape'
|
||||
_ = retAll
|
||||
if not solver:
|
||||
if utils.isLinearEdge(shape) or utils.isPlanar(shape):
|
||||
if utils.isLinearEdge(shape) or \
|
||||
utils.isPlanar(shape) or \
|
||||
utils.isCylindricalPlane(shape):
|
||||
return
|
||||
return 'a linear edge or edge/face with planar surface'
|
||||
return 'a linear edge or edge/face with planar or cylindrical surface'
|
||||
if utils.isLinearEdge(shape):
|
||||
return _l(solver,partInfo,subname,shape,False)
|
||||
if utils.isCylindricalPlane(shape):
|
||||
return _n(solver,partInfo,subname,shape,False)
|
||||
return _wa(solver,partInfo,subname,shape)
|
||||
|
||||
def _w(solver,partInfo,subname,shape,retAll=False,noCheck=False):
|
||||
|
@ -1171,7 +1175,8 @@ class BaseMulti(Base):
|
|||
e = cls._entityDef[0](
|
||||
solver,partInfo,info.Subname,info.Shape)
|
||||
params = props + [e0,e]
|
||||
solver.system.checkRedundancy(obj,partInfo0,partInfo)
|
||||
solver.system.checkRedundancy(
|
||||
obj,partInfo0,partInfo,info0.SubnameRef,info.SubnameRef)
|
||||
h = func(*params,group=solver.group)
|
||||
if isinstance(h,(list,tuple)):
|
||||
ret += list(h)
|
||||
|
@ -1224,16 +1229,19 @@ class BaseMulti(Base):
|
|||
if i==idx0:
|
||||
e0 = cls._entityDef[idx0](
|
||||
solver,partInfo,info.Subname,info.Shape)
|
||||
subname0 = info.SubnameRef
|
||||
info0 = partInfo
|
||||
else:
|
||||
e = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape)
|
||||
if e0 and e:
|
||||
if idx0:
|
||||
params = props + [e,e0]
|
||||
solver.system.checkRedundancy(obj,partInfo,info0)
|
||||
solver.system.checkRedundancy(
|
||||
obj,partInfo,info0,info.SubnameRef,subname0)
|
||||
else:
|
||||
params = props + [e0,e]
|
||||
solver.system.checkRedundancy(obj,info0,partInfo)
|
||||
solver.system.checkRedundancy(
|
||||
obj,info0,partInfo,subname0,info.SubnameRef)
|
||||
h = func(*params,group=solver.group)
|
||||
if isinstance(h,(list,tuple)):
|
||||
ret += list(h)
|
||||
|
@ -1266,7 +1274,8 @@ class BaseCascade(BaseMulti):
|
|||
params = props + [e1,e2]
|
||||
else:
|
||||
params = props + [e2,e1]
|
||||
solver.system.checkRedundancy(obj,prevInfo,partInfo)
|
||||
solver.system.checkRedundancy(
|
||||
obj,prevInfo,partInfo,prev.SubnameRef,info.SubnameRef)
|
||||
h = func(*params,group=solver.group)
|
||||
if isinstance(h,(list,tuple)):
|
||||
ret += list(h)
|
||||
|
|
|
@ -10,6 +10,16 @@ from .utils import getElementPos,objName,addIconToFCAD,guilogger as logger
|
|||
from .proxy import ProxyType
|
||||
from .FCADLogger import FCADLogger
|
||||
|
||||
def _isCommandActive(cmd):
|
||||
try:
|
||||
return FreeCADGui.Command.isActive(FreeCADGui.Command.get(cmd))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return FreeCADGui.isCommandActive(cmd)
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
class SelectionObserver:
|
||||
def __init__(self):
|
||||
self._attached = False
|
||||
|
@ -38,9 +48,13 @@ class SelectionObserver:
|
|||
res = sobj.Proxy.parent.Object.isElementVisible(sobj.Name)
|
||||
if res and vis:
|
||||
return False
|
||||
if not res and not vis:
|
||||
return;
|
||||
sobj.Proxy.parent.Object.setElementVisible(sobj.Name,vis)
|
||||
elif isTypeOf(sobj,AsmConstraint):
|
||||
vis = [vis] * len(flattenGroup(sobj))
|
||||
if sobj.VisibilityList == tuple(vis):
|
||||
return
|
||||
sobj.setPropertyStatus('VisibilityList','-Immutable')
|
||||
sobj.VisibilityList = vis
|
||||
sobj.setPropertyStatus('VisibilityList','Immutable')
|
||||
|
@ -103,7 +117,9 @@ class SelectionObserver:
|
|||
hasSelection = FreeCADGui.Selection.hasSelection()
|
||||
for cmd in self.cmds:
|
||||
cmd.onSelectionChange(hasSelection)
|
||||
FreeCADGui.updateCommands()
|
||||
update = getattr(FreeCADGui, 'updateCommands', None)
|
||||
if update:
|
||||
update()
|
||||
|
||||
def addSelection(self,docname,objname,subname,_pos):
|
||||
self.onChange()
|
||||
|
@ -250,6 +266,7 @@ class AsmCmdBase(with_metaclass(AsmCmdManager, object)):
|
|||
_contextMenuName = 'Assembly'
|
||||
_accel = None
|
||||
_cmdType = None
|
||||
_iconName = None
|
||||
|
||||
@classmethod
|
||||
def checkActive(cls):
|
||||
|
@ -257,15 +274,18 @@ class AsmCmdBase(with_metaclass(AsmCmdManager, object)):
|
|||
|
||||
@classmethod
|
||||
def getIconName(cls):
|
||||
return addIconToFCAD(cls._iconName)
|
||||
if cls._iconName:
|
||||
return addIconToFCAD(cls._iconName)
|
||||
|
||||
@classmethod
|
||||
def GetResources(cls):
|
||||
ret = {
|
||||
'Pixmap':cls.getIconName(),
|
||||
'MenuText':cls.getMenuText(),
|
||||
'ToolTip':cls.getToolTip()
|
||||
}
|
||||
name = cls.getIconName()
|
||||
if name:
|
||||
ret['Pixmap'] = name
|
||||
if cls._accel:
|
||||
ret['Accel'] = cls._accel
|
||||
if cls._cmdType is not None:
|
||||
|
@ -853,6 +873,7 @@ class AsmCmdGotoRelation(AsmCmdBase):
|
|||
_iconName = 'Assembly_GotoRelation.svg'
|
||||
_accel = 'A, R'
|
||||
_toolbarName = ''
|
||||
_cmdType = 'NoTransaction'
|
||||
|
||||
@classmethod
|
||||
def Activated(cls):
|
||||
|
@ -950,7 +971,7 @@ class AsmCmdGotoLinked(AsmCmdBase):
|
|||
|
||||
@classmethod
|
||||
def IsActive(cls):
|
||||
return FreeCADGui.isCommandActive('Std_LinkSelectLinked')
|
||||
return _isCommandActive('Std_LinkSelectLinked')
|
||||
|
||||
class AsmCmdGotoLinkedFinal(AsmCmdBase):
|
||||
_id = 23
|
||||
|
@ -1008,7 +1029,7 @@ class AsmCmdGotoLinkedFinal(AsmCmdBase):
|
|||
obj = sels[0].Object.getSubObject(sels[0].SubElementNames[0],1)
|
||||
if isTypeOf(obj, (AsmElementLink,AsmElement)):
|
||||
return True
|
||||
return FreeCADGui.isCommandActive('Std_LinkSelectLinkedFinal')
|
||||
return _isCommandActive('Std_LinkSelectLinkedFinal')
|
||||
|
||||
class AsmCmdUp(AsmCmdBase):
|
||||
_id = 6
|
||||
|
@ -1096,3 +1117,38 @@ class AsmCmdMultiply(AsmCmdBase):
|
|||
@classmethod
|
||||
def onSelectionChange(cls,hasSelection):
|
||||
cls._active = None if hasSelection else False
|
||||
|
||||
class AsmCmdToggleConstraint(AsmCmdBase):
|
||||
_id = 32
|
||||
_menuText = 'Toggle constraints'
|
||||
_toolbarName = None
|
||||
_menuGroupName = None
|
||||
_contextMenuName = None
|
||||
|
||||
@classmethod
|
||||
def checkActive(cls):
|
||||
from .assembly import isTypeOf, AsmConstraint
|
||||
cls._active = False
|
||||
count = 0
|
||||
for obj in FreeCADGui.Selection.getSelection('*'):
|
||||
if not isTypeOf(obj, AsmConstraint):
|
||||
return
|
||||
count += 1
|
||||
cls._active = count > 1
|
||||
|
||||
@classmethod
|
||||
def onSelectionChange(cls,hasSelection):
|
||||
cls._active = None if hasSelection else False
|
||||
|
||||
@classmethod
|
||||
def Activated(cls):
|
||||
from .assembly import isTypeOf, AsmConstraint
|
||||
FreeCAD.setActiveTransaction('Toggle constraints')
|
||||
try:
|
||||
for obj in FreeCADGui.Selection.getSelection('*'):
|
||||
if isTypeOf(obj, AsmConstraint):
|
||||
obj.Disabled = not obj.Disabled
|
||||
FreeCAD.closeActiveTransaction()
|
||||
except Exception:
|
||||
FreeCAD.closeActiveTransaction(True)
|
||||
raise
|
||||
|
|
|
@ -43,17 +43,15 @@ class Assembly3Workbench(FreeCADGui.Workbench):
|
|||
from .gui import AsmCmdManager,AsmCmdGotoRelation,\
|
||||
AsmCmdGotoLinked, AsmCmdGotoLinkedFinal
|
||||
AsmCmdManager.init()
|
||||
cmdSet = set()
|
||||
for name,cmds in AsmCmdManager.Toolbars.items():
|
||||
cmdSet.update(cmds)
|
||||
self.appendToolbar(name,[cmd.getName() for cmd in cmds])
|
||||
self.appendToolbar('Assembly3 Navigation', [
|
||||
AsmCmdGotoRelation.getName(), AsmCmdGotoLinked.getName(),
|
||||
AsmCmdGotoLinkedFinal.getName()])
|
||||
for name,cmds in AsmCmdManager.Menus.items():
|
||||
cmdSet.update(cmds)
|
||||
self.appendMenu(name,[cmd.getName() for cmd in cmds])
|
||||
self._observer.setCommands(cmdSet)
|
||||
|
||||
self._observer.setCommands(AsmCmdManager.getInfo().Types)
|
||||
# FreeCADGui.addPreferencePage(
|
||||
# ':/assembly3/ui/assembly3_prefs.ui','Assembly3')
|
||||
|
||||
|
@ -68,7 +66,12 @@ class Assembly3Workbench(FreeCADGui.Workbench):
|
|||
for name,cmds in menus.items():
|
||||
self.appendContextMenu(name,cmds)
|
||||
|
||||
def ContextMenu(self, _recipient):
|
||||
def ContextMenu(self, recipient):
|
||||
if recipient == 'Tree':
|
||||
from .gui import AsmCmdToggleConstraint
|
||||
if AsmCmdToggleConstraint.IsActive():
|
||||
self.appendContextMenu([],AsmCmdToggleConstraint.getName())
|
||||
|
||||
logger.catch('',self._contextMenu)
|
||||
|
||||
FreeCADGui.addWorkbench(Assembly3Workbench)
|
||||
|
|
|
@ -15,7 +15,7 @@ class AsmMovingPart(object):
|
|||
def __init__(self, moveInfo, element, moveElement):
|
||||
hierarchy = moveInfo.HierarchyList
|
||||
info = moveInfo.ElementInfo
|
||||
self.objs = [h.Assembly for h in reversed(hierarchy)]
|
||||
self.objs = [h.Assembly.getLinkedObject(True) for h in reversed(hierarchy)]
|
||||
self.assembly = resolveAssembly(info.Parent)
|
||||
self.viewObject = self.assembly.Object.ViewObject
|
||||
self.info = info
|
||||
|
@ -235,6 +235,9 @@ class AsmMovingPart(object):
|
|||
setPlacement(info.Part,pla)
|
||||
rollback.append((info.PartName,info.Part,info.Placement.copy()))
|
||||
|
||||
if QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ShiftModifier:
|
||||
return
|
||||
|
||||
if not gui.AsmCmdManager.AutoRecompute or \
|
||||
QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ControlModifier:
|
||||
# AsmCmdManager.AutoRecompute means auto re-solve the system. The
|
||||
|
|
|
@ -18,15 +18,12 @@ from .system import System
|
|||
# plane of the part.
|
||||
# EntityMap: string -> entity handle map, for caching
|
||||
# Group: transforming entity group handle
|
||||
# CstrMap: map from other part to the constrain between this and the other part.
|
||||
# This is for auto constraint DOF reduction. Only some composite
|
||||
# constraints will be mapped.
|
||||
# Update: in case the constraint uses the `Multiplication` feature, only the
|
||||
# first element of all the coplanar edges will be actually constrainted.
|
||||
# The rest ElementInfo will be stored here for later update by matrix
|
||||
# transformation.
|
||||
PartInfo = namedtuple('SolverPartInfo', ('Part','PartName','Placement',
|
||||
'Params','Workplane','EntityMap','Group','CstrMap','Update'))
|
||||
'Params','Workplane','EntityMap','Group','Update'))
|
||||
|
||||
class Solver(object):
|
||||
def __init__(self,assembly,reportFailed,dragPart,recompute,rollback):
|
||||
|
@ -315,7 +312,6 @@ class Solver(object):
|
|||
Workplane = h,
|
||||
EntityMap = {},
|
||||
Group = group if group else g,
|
||||
CstrMap = {},
|
||||
Update = [])
|
||||
|
||||
self.system.log('{}, {}',partInfo,g)
|
||||
|
|
|
@ -115,6 +115,14 @@ class SystemBase(with_metaclass(System, object)):
|
|||
self.verbose = obj.Verbose
|
||||
self.log = logger.info if obj.Verbose else logger.debug
|
||||
|
||||
def _cstrKey(cstrType, firstPart, secondPart):
|
||||
if firstPart > secondPart:
|
||||
return (cstrType, secondPart, firstPart)
|
||||
else:
|
||||
return (cstrType, firstPart, secondPart)
|
||||
|
||||
# For skipping invalid constraints
|
||||
_DummyCstrList = [None] * 6
|
||||
|
||||
class SystemExtension(object):
|
||||
def __init__(self):
|
||||
|
@ -125,9 +133,17 @@ class SystemExtension(object):
|
|||
self.firstInfo = None
|
||||
self.secondInfo = None
|
||||
self.relax = False
|
||||
self.coincidences = {}
|
||||
self.cstrMap = {}
|
||||
self.elementCstrMap = {}
|
||||
self.elementMap = {}
|
||||
self.firstElement = None
|
||||
self.secondElement = None
|
||||
|
||||
def checkRedundancy(self,obj,firstInfo,secondInfo):
|
||||
def checkRedundancy(self,obj,firstInfo,secondInfo,firstElement,secondElement):
|
||||
self.cstrObj,self.firstInfo,self.secondInfo=obj,firstInfo,secondInfo
|
||||
self.firstElement = firstElement
|
||||
self.secondElement = secondElement
|
||||
|
||||
def addSketchPlane(self,*args,**kargs):
|
||||
_ = kargs
|
||||
|
@ -147,34 +163,121 @@ class SystemExtension(object):
|
|||
h.append(self.addSameOrientation(n1.entity,n,group=group))
|
||||
return h
|
||||
|
||||
def reportRedundancy(self,warn=False):
|
||||
def reportRedundancy(self,firstPart=None,secondPart=None,count=0,limit=0,implicit=False):
|
||||
msg = '{} between {} and {}'.format(cstrName(self.cstrObj),
|
||||
self.firstInfo.PartName, self.secondInfo.PartName)
|
||||
if warn:
|
||||
logger.warn('skip redundant {}', msg, frame=1)
|
||||
firstPart if firstPart else self.firstInfo.PartName,
|
||||
secondPart if secondPart else self.secondInfo.PartName)
|
||||
if implicit:
|
||||
logger.msg('redundant implicit constraint {}, {}', msg, count, frame=1)
|
||||
elif count > limit:
|
||||
logger.warn('skip redundant {}, {}', msg, count, frame=1)
|
||||
else:
|
||||
logger.debug('auto relax {}', msg, frame=1)
|
||||
logger.msg('auto relax {}, {}', msg, count, frame=1)
|
||||
|
||||
def _countConstraints(self,increment,limit,*names):
|
||||
first,second = self.firstInfo,self.secondInfo
|
||||
if not first or not second:
|
||||
return []
|
||||
for name in names:
|
||||
cstrs = first.CstrMap.get(second.Part,{}).get(name,None)
|
||||
if not cstrs:
|
||||
if increment:
|
||||
cstrs = second.CstrMap.setdefault(
|
||||
first.Part,{}).setdefault(name,[])
|
||||
else:
|
||||
cstrs = second.CstrMap.get(first.Part,{}).get(name,[])
|
||||
cstrs += [None]*increment
|
||||
count = len(cstrs)
|
||||
if limit and count>=limit:
|
||||
self.reportRedundancy(count>limit)
|
||||
def _populateConstraintMap(
|
||||
self,cstrType,firstElement,secondElement,increment,limit,item,implicit):
|
||||
|
||||
firstPart = self.elementMap[firstElement]
|
||||
secondPart = self.elementMap[secondElement]
|
||||
if firstPart == secondPart:
|
||||
return _DummyCstrList
|
||||
|
||||
# A constraint may contain elements belong to more than two parts. For
|
||||
# exmaple, for a constraint with elements from part A, B, C, we'll
|
||||
# expand it into two constraints for parts AB and AC. However, we must
|
||||
# also count the implicit constraint between B and C.
|
||||
#
|
||||
# self.cstrMap is a map for counting constraints of the same type
|
||||
# between pairs of parts. The count is used for checking redundancy and
|
||||
# auto relaxing. The map is keyed using
|
||||
#
|
||||
# tuple(cstrType, firstPartName, secondPartName)
|
||||
#
|
||||
# and the value is a list. The item of this list is constraint defined
|
||||
# (e.g. PlaineAilgnment stores a plane entity as item for auto
|
||||
# relaxing) , the length of this list is use as the constraint count to
|
||||
# be used later to decide how to auto relax the constraint.
|
||||
#
|
||||
# See the following link for difficulties on auto relaxing with implicit
|
||||
# constraints. Right now there is no search performed. So the auto relax
|
||||
# may fail. And the user is required to manually reorder constraints and
|
||||
# the elements within to help the solver.
|
||||
#
|
||||
# https://github.com/realthunder/FreeCAD_assembly3/issues/403#issuecomment-757400349
|
||||
|
||||
key = _cstrKey(cstrType,firstPart,secondPart)
|
||||
cstrs = self.cstrMap.setdefault(key, [])
|
||||
cstrs += [item]*increment
|
||||
count = len(cstrs)
|
||||
if increment and count>=limit:
|
||||
self.reportRedundancy(firstPart, secondPart, count, limit, implicit)
|
||||
return cstrs
|
||||
|
||||
def countConstraints(self,increment,limit,*names):
|
||||
count = len(self._countConstraints(increment,limit,*names))
|
||||
def _countConstraints(self,increment,limit,cstrType,item=None):
|
||||
first, second = self.firstInfo, self.secondInfo
|
||||
if not first or not second:
|
||||
return []
|
||||
|
||||
firstElement, secondElement = self.firstElement, self.secondElement
|
||||
|
||||
if firstElement == secondElement:
|
||||
return _DummyCstrList
|
||||
|
||||
self.elementMap[firstElement] = first.PartName
|
||||
self.elementMap[secondElement] = second.PartName
|
||||
|
||||
# When counting implicit constraints (see comments in
|
||||
# _populateConstraintMap() above), we must also make sure to count them
|
||||
# if and only if they are originated from the same element, i.e. both
|
||||
# AB and AC involving the same element of A. This will be ture if the
|
||||
# those constraints are expanded by us, but may not be so if the user
|
||||
# created them.
|
||||
#
|
||||
# self.elementCstrMap is a map keyed using tuple(cstrType, elementName),
|
||||
# with value of a set of all element names that is involved with the the
|
||||
# same type of constraint. This set is shared by all element entries in
|
||||
# the map.
|
||||
|
||||
firstSet = self.elementCstrMap.setdefault((cstrType, firstElement), set())
|
||||
if not firstSet:
|
||||
firstSet.add(firstElement)
|
||||
secondSet = self.elementCstrMap.setdefault((cstrType, secondElement),firstSet)
|
||||
|
||||
res = _DummyCstrList
|
||||
|
||||
if firstSet is not secondSet:
|
||||
# If the secondSet is different, we shall merge them, and count the
|
||||
# implicit constraints between the elements of first and second set.
|
||||
for element in secondSet:
|
||||
self.elementCstrMap[(cstrType, element)] = firstSet
|
||||
is_second = element == secondElement
|
||||
for e in firstSet:
|
||||
implicit = not is_second or e != firstElement
|
||||
cstrs = self._populateConstraintMap(
|
||||
cstrType,e,element,increment,limit,item,implicit)
|
||||
if not implicit:
|
||||
# save the result (i.e. the explicit constraint pair of
|
||||
# the give first and second element) for return
|
||||
res = cstrs
|
||||
firstSet |= secondSet
|
||||
elif secondElement not in firstSet:
|
||||
# Here means the entry of the secondElement is newly created, count
|
||||
# the implicit constraints between all elements in the set to the
|
||||
# secondElement.
|
||||
for e in firstSet:
|
||||
implicit = e != firstElement
|
||||
cstrs = self._populateConstraintMap(
|
||||
cstrType,e,secondElement,increment,limit,item,implicit)
|
||||
if not implicit:
|
||||
res = cstrs
|
||||
firstSet.add(secondElement)
|
||||
|
||||
if res is _DummyCstrList:
|
||||
self.reportRedundancy(count=len(res), limit=limit)
|
||||
return res
|
||||
|
||||
def countConstraints(self,increment,limit,name):
|
||||
count = len(self._countConstraints(increment,limit,name))
|
||||
if count>limit:
|
||||
return -1
|
||||
return count
|
||||
|
@ -189,6 +292,10 @@ class SystemExtension(object):
|
|||
if count < 0:
|
||||
return
|
||||
|
||||
if count == 1:
|
||||
self.coincidences[(self.firstInfo.Part, self.secondInfo.Part)] = pln1
|
||||
self.coincidences[(self.secondInfo.Part, self.firstInfo.Part)] = pln2
|
||||
|
||||
if d or dx or dy:
|
||||
dx,dy,d = pln2.normal.rot.multVec(FreeCAD.Vector(dx,dy,d))
|
||||
v = pln2.origin.vector+FreeCAD.Vector(dx,dy,d)
|
||||
|
@ -200,23 +307,21 @@ class SystemExtension(object):
|
|||
|
||||
if not lockAngle and count==2:
|
||||
# if there is already some other plane coincident constraint set for
|
||||
# this pair of parts, we reduce this second constraint to either a
|
||||
# points horizontal or vertical constraint, i.e. reduce the
|
||||
# constraining DOF down to 1.
|
||||
# this pair of parts, we reduce this second constraint to a 2D
|
||||
# PointOnLine. The line is formed by the first part's two elements
|
||||
# in the previous and the current constraint. The point is taken
|
||||
# from the element of the second part of the current constraint.
|
||||
# The projection plane is taken from the element of the first part
|
||||
# of the current constraint.
|
||||
#
|
||||
# We project the initial points to the first element plane, and
|
||||
# check for differences in x and y components of the points to
|
||||
# determine whether to use horizontal or vertical constraint.
|
||||
rot = pln1.normal.pla.Rotation.multiply(pln1.normal.rot)
|
||||
v1 = pln1.normal.pla.multVec(pln1.origin.vector)
|
||||
v2 = pln2.normal.pla.multVec(v)
|
||||
v1,v2 = project2D(rot, v1, v2)
|
||||
if abs(v1.x-v2.x) < abs(v1.y-v2.y):
|
||||
h.append(self.addPointsHorizontal(
|
||||
pln1.origin.entity, e, pln1.entity, group=group))
|
||||
else:
|
||||
h.append(self.addPointsVertical(
|
||||
pln1.origin.entity, e, pln1.entity, group=group))
|
||||
# This 2D PointOnLine effectively reduce the second PlaneCoincidence
|
||||
# constraining DOF down to 1.
|
||||
prev = self.coincidences.get(
|
||||
(self.firstInfo.Part, self.secondInfo.Part))
|
||||
ln = self.addLineSegment(prev.origin.entity,
|
||||
pln1.origin.entity, group=self.firstInfo.Group)
|
||||
h.append(self.addPointOnLine(
|
||||
pln2.origin.entity, ln, pln1.entity, group=group))
|
||||
return h
|
||||
|
||||
h.append(self.addPointsCoincident(pln1.origin.entity, e, group=group))
|
||||
|
@ -225,7 +330,7 @@ class SystemExtension(object):
|
|||
pln1.normal, pln2.normal, group)
|
||||
|
||||
def addAttachment(self, pln1, pln2, group=0):
|
||||
return self.addPlaneCoincident(0,0,0,False,0,0,0, pln1, pln2, group)
|
||||
return self.addPlaneCoincident(0,0,0,True,0,0,0, pln1, pln2, group)
|
||||
|
||||
def addPlaneAlignment(self,d,lockAngle,yaw,pitch,roll,pln1,pln2,group=0):
|
||||
if not group:
|
||||
|
@ -233,15 +338,12 @@ class SystemExtension(object):
|
|||
h = []
|
||||
if self.relax:
|
||||
dof = 2 if lockAngle else 1
|
||||
cstrs = self._countConstraints(dof,3,'Alignment')
|
||||
cstrs = self._countConstraints(dof,3,'Alignment',item=pln1.entity)
|
||||
count = len(cstrs)
|
||||
if count > 3:
|
||||
return
|
||||
if count == 1:
|
||||
cstrs[0] = pln1.entity
|
||||
else:
|
||||
count = 0
|
||||
cstrs = None
|
||||
|
||||
if d:
|
||||
h.append(self.addPointPlaneDistance(
|
||||
|
@ -252,7 +354,7 @@ class SystemExtension(object):
|
|||
if count<=2:
|
||||
n1,n2 = pln1.normal,pln2.normal
|
||||
if count==2 and not lockAngle:
|
||||
self.reportRedundancy()
|
||||
self.reportRedundancy(count=count, limit=count)
|
||||
h.append(self.addParallel(n2.entity,n1.entity,cstrs[0],group))
|
||||
else:
|
||||
self.setOrientation(h,lockAngle,yaw,pitch,roll,n1,n2,group)
|
||||
|
|
|
@ -77,7 +77,10 @@ def addIconToFCAD(iconFile,path=None):
|
|||
|
||||
def objName(obj):
|
||||
try:
|
||||
return getattr(obj,'FullName',obj.Name)
|
||||
name = getattr(obj,'FullName',obj.Name)
|
||||
if obj.Label != obj.Name:
|
||||
name = '%s (%s)' % (name, obj.Label)
|
||||
return name
|
||||
except Exception:
|
||||
return '?'
|
||||
|
||||
|
@ -85,7 +88,7 @@ def isLine(param):
|
|||
return isinstance(param,(Part.Line,Part.LineSegment))
|
||||
|
||||
def deduceSelectedElement(obj,subname):
|
||||
shape = obj.getSubObject(subname)
|
||||
shape = getElementShape(obj, subname)
|
||||
if not shape:
|
||||
return
|
||||
count = shape.countElement('Face')
|
||||
|
@ -207,6 +210,14 @@ def isElement(obj):
|
|||
|
||||
def getElement(shape, element):
|
||||
res = None
|
||||
if not isinstance(shape, Part.Shape):
|
||||
try:
|
||||
res = getElementShape(shape, element)
|
||||
if res and not res.isNull():
|
||||
return res
|
||||
except Exception:
|
||||
return
|
||||
|
||||
try:
|
||||
res = shape.getElement(element, True)
|
||||
except TypeError:
|
||||
|
|
Loading…
Reference in New Issue
Block a user