Compare commits

...

31 Commits
0.11 ... master

Author SHA1 Message Date
Zheng, Lei
bc2d5d611f solver: improve redundancy checking of implicit constraints
Related #403
2021-01-11 10:00:10 +08:00
Zheng, Lei
e785510c68 assembly: support drag and drop reordering of constraint and elements 2021-01-10 13:03:25 +08:00
Zheng, Lei
b9c7af40fd assembly: fix 'Flip element' for multiplied constraining element 2021-01-08 20:56:32 +08:00
Zheng, Lei
b590b57793 mover: fix auto recompute on moving linked assembly 2021-01-08 17:24:39 +08:00
Zheng, Lei
6c68f0f901 assembly: support flip part of multiplied constraining elements 2021-01-08 17:22:56 +08:00
Zheng, Lei
caad48e927 assembly: reduce constraint multiplication init placement precision 2021-01-08 16:18:04 +08:00
Zheng, Lei
70d161f336 assembly: fix relation selection highlight when using GoToRelation 2021-01-05 15:25:43 +08:00
Zheng, Lei
c6f6c481d2 assembly: fix stray relations when deleting assembly 2021-01-05 15:25:34 +08:00
Zheng, Lei
fe6a792202 assembly: fix workplane orientation
Fixes #362
2020-12-01 20:40:19 +08:00
Zheng, Lei
da050987f5 assembly: fix constraint multiplication with sub-assembly
Related #341
2020-11-16 20:52:45 +08:00
Zheng, Lei
6fbded4460 assembly: fix freeze toggling behavior
Related #342
2020-11-16 20:50:40 +08:00
Zheng, Lei
6bb16b5550 system: improve auto relax multiple PlaneCoincidence 2020-11-12 20:58:36 +08:00
lilaL
c3380e602d Typo fix 2020-11-12 20:57:48 +08:00
Zheng, Lei
9f4fa40b8f constraint: support cylindrical face in MultiParallel 2020-10-26 20:37:34 +08:00
Zheng, Lei
4edc0fbca6 Gui: fix call of FreeCADGui.Command.isActive() 2020-09-26 16:25:01 +08:00
Zheng, Lei
490da82590 gui: fix command active detection 2020-09-26 09:33:06 +08:00
Zheng, Lei
e44f47073b system: lock angle when convert Attachment to PlaneCoincident 2020-08-04 09:08:55 +08:00
Zheng, Lei
0c1f659fde assembly: fix constraint multiply element update 2020-07-28 18:20:01 +08:00
Zheng, Lei
5da0bd5554 assembly: improve AsmElement fix element 2020-07-28 18:20:01 +08:00
Zheng, Lei
14dffc7b28 assembly: fix ViewProviderAsmElementLink bounding box calculation 2020-07-28 18:20:01 +08:00
Zheng, Lei
652ea7af6c assembly: implement ViewProviderAsmElement(Link).getLinkedViewProvider()
For better support the Std_LinkSelectLinked(Final) command
2020-07-28 18:19:53 +08:00
Zheng, Lei
c8cf0a1e32 gui: fix infinite recursion triggered by auto element visibility 2020-07-04 22:04:08 +08:00
Zheng, Lei
19163afe88 gui: disable transaction in command AsmCmdGotoRelation 2020-07-02 11:23:27 +08:00
Zheng, Lei
0acaa5e9c3 Avoid using getSubObject() to obtain shape 2020-07-02 09:17:24 +08:00
Zheng, Lei
9e34ecd1c0 mover: use SHIFT key to bypass recompute on moving 2020-06-29 17:36:54 +08:00
Zheng, Lei
3bd99b64d2 gui: clear up debug output 2020-06-15 16:46:25 +08:00
Zheng, Lei
9f904ede3f assembly: improve backward compatibility 2020-06-15 16:46:11 +08:00
Zheng, Lei
72f64e9ed5 assembly: fixed AsmPartGroup.getSubObjects() when partial loaded 2020-05-25 09:36:31 +00:00
Zheng, Lei
91c34b2fcb Add tree view context menu action for batch toggling constraints 2020-05-13 12:23:51 +08:00
Zheng, Lei
1cdf1fe5c4 assembly: fix assembly 'freeze'
Fixes #286
2020-05-06 08:19:15 +08:00
Zheng, Lei
edac36d4df gui: improve backward compatibility 2020-04-25 18:30:07 +08:00
8 changed files with 473 additions and 138 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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: