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,6 +102,13 @@ def editGroup(obj,children,notouch=None):
change = '-Immutable' change = '-Immutable'
revert = '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) parent = getattr(obj,'_Parent',None)
if parent and 'Touched' in parent.State: if parent and 'Touched' in parent.State:
parent = None parent = None
@ -109,12 +116,6 @@ def editGroup(obj,children,notouch=None):
if not hasProperty(obj,'NoTouch'): if not hasProperty(obj,'NoTouch'):
notouch = False notouch = False
elif notouch is None: elif notouch is None:
if (isTypeOf(parent,AsmConstraintGroup) or \
isTypeOf(obj,AsmConstraintGroup)):
# the order inside constraint group actually matters, so do not
# engage no touch
parent = None
else:
notouch = not obj.NoTouch notouch = not obj.NoTouch
if notouch: if notouch:
@ -122,6 +123,7 @@ def editGroup(obj,children,notouch=None):
block = gui.AsmCmdManager.AutoRecompute block = gui.AsmCmdManager.AutoRecompute
if block: if block:
gui.AsmCmdManager.AutoRecompute = False gui.AsmCmdManager.AutoRecompute = False
try: try:
if change: if change:
obj.setPropertyStatus('Group',change) obj.setPropertyStatus('Group',change)
@ -137,10 +139,10 @@ def editGroup(obj,children,notouch=None):
parent.purgeTouched() parent.purgeTouched()
def setupSortMenu(menu,func,func2): 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) QtCore.QObject.connect(action,QtCore.SIGNAL("triggered()"),func)
menu.addAction(action) 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( QtCore.QObject.connect(
action,QtCore.SIGNAL("triggered()"),func2) action,QtCore.SIGNAL("triggered()"),func2)
menu.addAction(action) menu.addAction(action)
@ -217,7 +219,10 @@ class ViewProviderAsmBase(object):
vobj.Proxy = self vobj.Proxy = self
self.attach(vobj) self.attach(vobj)
def replaceObject(self,_new,_old): def canReplaceObject(self, _old, _new):
return False
def replaceObject(self,_old,_new):
return False return False
def canAddToSceneGraph(self): def canAddToSceneGraph(self):
@ -285,7 +290,7 @@ class AsmGroup(AsmBase):
class ViewProviderAsmGroup(ViewProviderAsmBase): class ViewProviderAsmGroup(ViewProviderAsmBase):
def claimChildren(self): def claimChildren(self):
return self.ViewObject.Object.Group return getattr(self.ViewObject.Object, 'Group', [])
def doubleClicked(self, _vobj): def doubleClicked(self, _vobj):
return False return False
@ -293,6 +298,21 @@ class ViewProviderAsmGroup(ViewProviderAsmBase):
def canDropObject(self,_child): def canDropObject(self,_child):
return False 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): class ViewProviderAsmGroupOnTop(ViewProviderAsmGroup):
def __init__(self,vobj): def __init__(self,vobj):
@ -309,6 +329,8 @@ class AsmPartGroup(AsmGroup):
def getSubObjects(self,obj,_reason): def getSubObjects(self,obj,_reason):
# Deletion order problem may cause exception here. Just silence it # Deletion order problem may cause exception here. Just silence it
try: try:
if not getattr(obj.Document,'Partial',False) \
or not self.getAssembly().Object.Freeze:
return [ '{}.'.format(o.Name) for o in flattenGroup(obj) ] return [ '{}.'.format(o.Name) for o in flattenGroup(obj) ]
except Exception: except Exception:
pass pass
@ -317,6 +339,10 @@ class AsmPartGroup(AsmGroup):
super(AsmPartGroup,self).linkSetup(obj) super(AsmPartGroup,self).linkSetup(obj)
if not hasProperty(obj,'DerivedFrom'): if not hasProperty(obj,'DerivedFrom'):
obj.addProperty('App::PropertyLink','DerivedFrom','Base','') obj.addProperty('App::PropertyLink','DerivedFrom','Base','')
try:
obj.setPropertyStatus('Shape','-Output')
except Exception:
pass
self.derivedParts = None self.derivedParts = None
def checkDerivedParts(self): def checkDerivedParts(self):
@ -374,7 +400,6 @@ class AsmPartGroup(AsmGroup):
obj = parent.Document.addObject("Part::FeaturePython",name, obj = parent.Document.addObject("Part::FeaturePython",name,
AsmPartGroup(parent),None,True) AsmPartGroup(parent),None,True)
obj.setPropertyStatus('Placement',('Output','Hidden')) obj.setPropertyStatus('Placement',('Output','Hidden'))
obj.setPropertyStatus('Shape','Output')
ViewProviderAsmPartGroup(obj.ViewObject) ViewProviderAsmPartGroup(obj.ViewObject)
obj.purgeTouched() obj.purgeTouched()
return obj return obj
@ -383,9 +408,6 @@ class AsmPartGroup(AsmGroup):
class ViewProviderAsmPartGroup(ViewProviderAsmGroup): class ViewProviderAsmPartGroup(ViewProviderAsmGroup):
_iconName = 'Assembly_Assembly_Part_Tree.svg' _iconName = 'Assembly_Assembly_Part_Tree.svg'
def replaceObject(self,new,old):
return self.Object.replaceObject(new,old)
def canDropObjectEx(self,obj,_owner,_subname,_elements): def canDropObjectEx(self,obj,_owner,_subname,_elements):
return isTypeOf(obj,Assembly, True) or not isTypeOf(obj,AsmBase) return isTypeOf(obj,Assembly, True) or not isTypeOf(obj,AsmBase)
@ -445,6 +467,9 @@ class ViewProviderAsmPartGroup(ViewProviderAsmGroup):
pass pass
vobj.DefaultMode = mode vobj.DefaultMode = mode
def canReplaceObject(self, _old, _new):
return True
def replaceObject(self,oldObj,newObj): def replaceObject(self,oldObj,newObj):
res = self.ViewObject.replaceObject(oldObj,newObj) res = self.ViewObject.replaceObject(oldObj,newObj)
if res<=0: if res<=0:
@ -620,12 +645,7 @@ class AsmElement(AsmBase):
if not obj: if not obj:
return False # broken beyond fix return False # broken beyond fix
subs = Part.splitSubname(subname) if not utils.getElement(linked, subname):
if not subs[1]:
return False # no mapped element name
shape = linked.getSubObject(subs[0])
if not utils.getElement(shape, subs[1]):
return True return True
def fix(self): def fix(self):
@ -640,7 +660,7 @@ class AsmElement(AsmBase):
if not subs[1]: if not subs[1]:
raise RuntimeError('No mapped sub-element found') raise RuntimeError('No mapped sub-element found')
shape = linked.getSubObject(subs[0]) shape = Part.getShape(linked,subs[0])
if utils.getElement(shape, subs[1]): if utils.getElement(shape, subs[1]):
return return
@ -685,6 +705,10 @@ class AsmElement(AsmBase):
self.version.value += 1 self.version.value += 1
return False return False
if self.getAssembly().Object.Freeze:
logger.warn('Skip recomputing frozen element {}', objName(obj))
return True
if obj.Detach: if obj.Detach:
self.updatePlacement() self.updatePlacement()
return True return True
@ -692,7 +716,9 @@ class AsmElement(AsmBase):
info = None info = None
try: try:
info = self.getInfo(False) info = self.getInfo(False)
except Exception: except Exception as e:
logger.warn(str(e))
self.updatePlacement() self.updatePlacement()
if not gui.AsmCmdManager.AutoFixElement: if not gui.AsmCmdManager.AutoFixElement:
@ -809,10 +835,13 @@ class AsmElement(AsmBase):
link = self.Object.LinkedObject link = self.Object.LinkedObject
if not isinstance(link,tuple): if not isinstance(link,tuple):
raise RuntimeError('Borken element link') raise RuntimeError('Broken element link')
obj = link[0].getSubObject(link[1],1) obj = link[0].getSubObject(link[1],1)
if not obj: 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 isTypeOf(obj,AsmElement):
# If not pointing to another element, then assume we are directly # If not pointing to another element, then assume we are directly
# pointing to the geometry element, just return as it is, which is a # 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'): if not vobj or hasProperty(vobj.Object,'Radius'):
return return
if getattr(vobj,'ShowCS',False) or\ if getattr(vobj,'ShowCS',False) or\
gui.AsmCmdManager.ShowElementCS: gui.AsmCmdManager.ShowElementCS or\
not hasattr(vobj.Object,'Shape'):
return True return True
return utils.isInfinite(vobj.Object.Shape) return utils.isInfinite(vobj.Object.Shape)
@ -1356,18 +1386,29 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop):
rot = FreeCAD.Rotation(FreeCAD.Vector(1,0,0),180) rot = FreeCAD.Rotation(FreeCAD.Vector(1,0,0),180)
rot = FreeCAD.Placement(FreeCAD.Vector(), rot) rot = FreeCAD.Placement(FreeCAD.Vector(), rot)
FreeCAD.setActiveTransaction( title = 'Flip element' if flipElement else 'Flip part'
'Flip element' if flipElement else 'Flip part') FreeCAD.setActiveTransaction(title)
try: try:
if flipElement: if flipElement:
obj.Offset = rot.multiply(obj.Offset) 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: else:
offset = utils.getElementPlacement(obj.getSubObject('')) offset = utils.getElementPlacement(obj.getSubObject(''))
offset = offset.multiply(rot).multiply(offset.inverse()) 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) obj.recompute(True)
FreeCAD.closeActiveTransaction() FreeCAD.closeActiveTransaction()
except Exception: except Exception:
QtGui.QMessageBox.critical(None, 'Flip', title + ' failed')
FreeCAD.closeActiveTransaction(True) FreeCAD.closeActiveTransaction(True)
raise raise
@ -1379,6 +1420,20 @@ class ViewProviderAsmElement(ViewProviderAsmOnTop):
obj = self.ViewObject.Object obj = self.ViewObject.Object
ViewProviderAsmElement.doFlip(obj, obj.Proxy.getInfo(), False) 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): class AsmElementSketch(AsmElement):
def __init__(self,obj,parent): def __init__(self,obj,parent):
super(AsmElementSketch,self).__init__(parent) super(AsmElementSketch,self).__init__(parent)
@ -1570,7 +1625,7 @@ def getElementInfo(parent,subname,
# no element object here. # no element object here.
part = (part[0],idx,part[1],True) part = (part[0],idx,part[1],True)
pla = part[0].Placement.multiply(plaList[idx]) pla = part[0].Placement.multiply(plaList[idx])
except ValueError: except Exception:
raise RuntimeError('invalid array subname of element ' raise RuntimeError('invalid array subname of element '
'{}: {}'.format(objName(parent),subnameRef)) '{}: {}'.format(objName(parent),subnameRef))
@ -1592,7 +1647,7 @@ def getElementInfo(parent,subname,
'{}.{}'.format(objName(part),subname)) '{}.{}'.format(objName(part),subname))
pla = getattr(part,'Placement',FreeCAD.Placement()) pla = getattr(part,'Placement',FreeCAD.Placement())
obj = part.getLinkedObject(False) obj = part.getLinkedObject(False)
partName = part.Name partName = objName(part)
if transformShape: if transformShape:
# Copy and transform shape. We have to copy the shape here to work # Copy and transform shape. We have to copy the shape here to work
@ -1648,6 +1703,11 @@ class AsmElementLink(AsmBase):
self.version = AsmVersion() 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): def migrate(self,obj):
link = obj.LinkedObject link = obj.LinkedObject
if not isinstance(link,tuple): if not isinstance(link,tuple):
@ -1771,6 +1831,17 @@ class AsmElementLink(AsmBase):
def getAssembly(self): def getAssembly(self):
return self.parent.parent.parent 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): def getElementSubname(self,recursive=False):
'Resolve element link subname' 'Resolve element link subname'
@ -1896,8 +1967,7 @@ class AsmElementLink(AsmBase):
info = self.info info = self.info
if obj.Offset.isIdentity(): if obj.Offset.isIdentity():
if not obj.Placement.isIdentity(): pla = FreeCAD.Placement()
obj.Placement = FreeCAD.Placement()
else: else:
# obj.Offset is in the element shape's coordinate system, we need to # obj.Offset is in the element shape's coordinate system, we need to
# transform it to the assembly coordinate system # transform it to the assembly coordinate system
@ -1905,8 +1975,6 @@ class AsmElementLink(AsmBase):
mOffset = obj.Offset.toMatrix() mOffset = obj.Offset.toMatrix()
mat = info.Placement.toMatrix()*mShape mat = info.Placement.toMatrix()*mShape
pla = FreeCAD.Placement(mat*mOffset*mat.inverse()) 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.Shape.transformShape(mShape*mOffset*mShape.inverse())
info = ElementInfo(Parent = info.Parent, info = ElementInfo(Parent = info.Parent,
@ -1922,6 +1990,9 @@ class AsmElementLink(AsmBase):
parent = self.parent.Object parent = self.parent.Object
if not Constraint.canMultiply(parent): 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.multiply = False
self.infos.append(info) self.infos.append(info)
return self.infos if expand else self.info return self.infos if expand else self.info
@ -1933,8 +2004,18 @@ class AsmElementLink(AsmBase):
self.infos.append(info) self.infos.append(info)
return self.infos if expand else self.info return self.infos if expand else self.info
infos = [] infos = []
offset = info.Placement.inverse()
plaList = [] 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): for i in range(obj.Count):
part = info.Part part = info.Part
if part[3]: if part[3]:
@ -1945,7 +2026,9 @@ class AsmElementLink(AsmBase):
pla = sobj.Placement pla = sobj.Placement
part = (part[0],i,sobj,part[3]) part = (part[0],i,sobj,part[3])
pla = part[0].Placement.multiply(pla) pla = part[0].Placement.multiply(pla)
plaList.append(pla.multiply(offset)) plaList.append(pla.multiply(offset))
infos.append(ElementInfo( infos.append(ElementInfo(
Parent = info.Parent, Parent = info.Parent,
SubnameRef = info.SubnameRef, SubnameRef = info.SubnameRef,
@ -1959,6 +2042,10 @@ class AsmElementLink(AsmBase):
self.infos = infos self.infos = infos
return infos if expand else info 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): for i,edge in enumerate(info.Shape.Edges):
self.infos.append(ElementInfo( self.infos.append(ElementInfo(
Parent = info.Parent, Parent = info.Parent,
@ -2093,6 +2180,22 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop):
obj = self.ViewObject.Object obj = self.ViewObject.Object
ViewProviderAsmElement.doFlip(obj, obj.Proxy.getInfo(), False) 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): class AsmConstraint(AsmGroup):
@ -2277,7 +2380,7 @@ class AsmConstraint(AsmGroup):
# element shape # element shape
offset = FreeCAD.Vector(getattr(obj,'OffsetX',0), offset = FreeCAD.Vector(getattr(obj,'OffsetX',0),
getattr(obj,'Offset&',0), getattr(obj,'OffsetY',0),
getattr(obj,'Offset',0)) getattr(obj,'Offset',0))
poses = poses[:count] poses = poses[:count]
infos0 = firstChild.Proxy.getInfo(expand=True)[:count] infos0 = firstChild.Proxy.getInfo(expand=True)[:count]
@ -2296,7 +2399,7 @@ class AsmConstraint(AsmGroup):
if i<len(prev) and prev[i]<count: if i<len(prev) and prev[i]<count:
j = prev[i] j = prev[i]
if used[i]<0 and not order[j] and \ if used[i]<0 and not order[j] and \
pos0.distanceToPoint(poses[j]) < 1e-7: pos0.distanceToPoint(poses[j]) < 1e-6:
distances[i] = 0 distances[i] = 0
if not elements[i]._refPla: if not elements[i]._refPla:
pla = infos[j].Placement.multiply( pla = infos[j].Placement.multiply(
@ -2313,7 +2416,7 @@ class AsmConstraint(AsmGroup):
if order[j]: if order[j]:
continue continue
d = pos0.distanceToPoint(pos) d = pos0.distanceToPoint(pos)
if used[i]<0 and d < 1e-7: if used[i]<0 and d < 1e-6:
distances[i] = 0 distances[i] = 0
if not elements[i]._refPla: if not elements[i]._refPla:
pla = infos[j].Placement.multiply( pla = infos[j].Placement.multiply(
@ -2347,6 +2450,10 @@ class AsmConstraint(AsmGroup):
firstChild.Proxy.infos = order firstChild.Proxy.infos = order
self.prevOrder = used self.prevOrder = used
from . import solver
if solver.isBusy():
return
# now for those instances that are 'out of place', lets assign some # now for those instances that are 'out of place', lets assign some
# initial placement # initial placement
@ -2376,14 +2483,21 @@ class AsmConstraint(AsmGroup):
if ref: if ref:
pla = pla.multiply(ref) pla = pla.multiply(ref)
else: else:
pla = info0.Placement.multiply(pla.multiply(pla0.inverse())) pla = pla.multiply(p0.inverse())
showPart(partGroup,info0.Part) showPart(partGroup,info0.Part)
touched = True touched = True
setPlacement(info0.Part,pla,True)
if touched: # DO NOT purgeTouched here. We shall leave it as touched and
firstChild.Proxy.getInfo(True) # trigger a second pass of recomputation to properly update the
firstChild.purgeTouched() # 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): def execute(self,obj):
if not getattr(self,'_initializing',False) and\ if not getattr(self,'_initializing',False) and\
@ -2688,7 +2802,17 @@ class AsmConstraint(AsmGroup):
setLinkProperty(info.Part,'ShowElement',False) setLinkProperty(info.Part,'ShowElement',False)
try: try:
setLinkProperty(info.Part,'ElementCount',1) 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: except Exception:
logger.error(traceback.format_exc())
raise RuntimeError('Failed to change element count of ' raise RuntimeError('Failed to change element count of '
'{}'.format(info.PartName)) '{}'.format(info.PartName))
@ -2759,7 +2883,7 @@ class ViewProviderAsmConstraint(ViewProviderAsmGroup):
def setupContextMenu(self,vobj,menu): def setupContextMenu(self,vobj,menu):
obj = vobj.Object obj = vobj.Object
action = QtGui.QAction(QtGui.QIcon(), action = QtGui.QAction(QtGui.QIcon(),
"Enable" if obj.Disabled else "Disable", menu) "Enable constraint" if obj.Disabled else "Disable constraint", menu)
QtCore.QObject.connect( QtCore.QObject.connect(
action,QtCore.SIGNAL("triggered()"),self.toggleDisable) action,QtCore.SIGNAL("triggered()"),self.toggleDisable)
menu.addAction(action) menu.addAction(action)
@ -3008,15 +3132,13 @@ class AsmRelationGroup(AsmBase):
super(AsmRelationGroup,self).__init__() super(AsmRelationGroup,self).__init__()
def attach(self,obj): def attach(self,obj):
# AsmRelationGroup do not install LinkBaseExtension
# obj.addExtension('App::LinkBaseExtensionPython', None)
obj.addProperty('App::PropertyLinkList','Group','') obj.addProperty('App::PropertyLinkList','Group','')
obj.setPropertyStatus('Group','Hidden') obj.setPropertyStatus('Group','Hidden')
obj.addProperty('App::PropertyLink','Constraints','') obj.addProperty('App::PropertyLink','Constraints','')
# this is to make sure relations are recomputed after all constraints # this is to make sure relations are recomputed after all constraints
obj.Constraints = self.parent.getConstraintGroup() obj.Constraints = self.parent.getConstraintGroup()
obj.setPropertyStatus('Constraints',('Hidden','Immutable')) obj.setPropertyStatus('Constraints',('Hidden','Immutable'))
self.linkSetup(obj) self.linkSetup(obj)
def getViewProviderName(self,_obj): def getViewProviderName(self,_obj):
@ -3024,6 +3146,20 @@ class AsmRelationGroup(AsmBase):
def linkSetup(self,obj): def linkSetup(self,obj):
super(AsmRelationGroup,self).linkSetup(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: for o in obj.Group:
o.Proxy.parent = self o.Proxy.parent = self
if o.Count: if o.Count:
@ -3440,15 +3576,26 @@ class ViewProviderAsmRelation(ViewProviderAsmBase):
def canDropObjects(self): def canDropObjects(self):
return False return False
def onDelete(self,_vobj,_subs):
return False
def canDelete(self,_obj): def canDelete(self,_obj):
return True return True
def claimChildren(self): def claimChildren(self):
return self.ViewObject.Object.Group 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' BuildShapeNone = 'None'
BuildShapeCompound = 'Compound' BuildShapeCompound = 'Compound'
@ -4203,11 +4350,12 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
def __init__(self,vobj): def __init__(self,vobj):
self._movingPart = None self._movingPart = None
super(ViewProviderAssembly,self).__init__(vobj) super(ViewProviderAssembly,self).__init__(vobj)
self.showParts()
def setupContextMenu(self,vobj,menu): def setupContextMenu(self,vobj,menu):
obj = vobj.Object obj = vobj.Object
action = QtGui.QAction(QtGui.QIcon(), action = QtGui.QAction(QtGui.QIcon(),
"Unfreeze" if obj.Freeze else "Freeze", menu) "Unfreeze assembly" if obj.Freeze else "Freeze assembly", menu)
QtCore.QObject.connect( QtCore.QObject.connect(
action,QtCore.SIGNAL("triggered()"),self.toggleFreeze) action,QtCore.SIGNAL("triggered()"),self.toggleFreeze)
menu.addAction(action) menu.addAction(action)
@ -4218,6 +4366,7 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
'Unfreeze assembly' if obj.Freeze else 'Freeze assembly') 'Unfreeze assembly' if obj.Freeze else 'Freeze assembly')
try: try:
obj.Freeze = not obj.Freeze obj.Freeze = not obj.Freeze
obj.recompute(True)
FreeCAD.closeActiveTransaction() FreeCAD.closeActiveTransaction()
except Exception: except Exception:
FreeCAD.closeActiveTransaction(True) FreeCAD.closeActiveTransaction(True)
@ -4237,6 +4386,12 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
def canDelete(self,obj): def canDelete(self,obj):
return isTypeOf(obj,AsmRelationGroup) return isTypeOf(obj,AsmRelationGroup)
def canReplaceObject(self, _old, _new):
return False
def replaceObject(self,_old,_new):
return False
def _convertSubname(self,owner,subname): def _convertSubname(self,owner,subname):
sub = subname.split('.') sub = subname.split('.')
if not sub: if not sub:
@ -4345,6 +4500,9 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
return False return False
def showParts(self): def showParts(self):
if not hasProperty(self.ViewObject,'ShowParts'):
self.ViewObject.addProperty("App::PropertyBool","ShowParts"," Link")
return
proxy = self.ViewObject.Object.Proxy proxy = self.ViewObject.Object.Proxy
if proxy: if proxy:
proxy.getPartGroup().ViewObject.Proxy.showParts() proxy.getPartGroup().ViewObject.Proxy.showParts()
@ -4365,9 +4523,6 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
self.showParts() self.showParts()
def finishRestoring(self): def finishRestoring(self):
if not hasProperty(self.ViewObject,'ShowParts'):
self.ViewObject.addProperty("App::PropertyBool","ShowParts"," Link")
else:
self.showParts() self.showParts()
@classmethod @classmethod
@ -4489,10 +4644,10 @@ class AsmWorkPlane(object):
else: else:
if tp==1: if tp==1:
pla = FreeCAD.Placement(info.Placement.Base, 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: elif tp==2:
pla = FreeCAD.Placement(info.Placement.Base, pla = FreeCAD.Placement(info.Placement.Base,
FreeCAD.Rotation(FreeCAD.Vector(1,0,0),90)) FreeCAD.Rotation(FreeCAD.Vector(0,1,0),-90))
else: else:
pla = info.Placement 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' 'return a handle for either a line or a plane depending on the shape'
_ = retAll _ = retAll
if not solver: 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
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): if utils.isLinearEdge(shape):
return _l(solver,partInfo,subname,shape,False) return _l(solver,partInfo,subname,shape,False)
if utils.isCylindricalPlane(shape):
return _n(solver,partInfo,subname,shape,False)
return _wa(solver,partInfo,subname,shape) return _wa(solver,partInfo,subname,shape)
def _w(solver,partInfo,subname,shape,retAll=False,noCheck=False): def _w(solver,partInfo,subname,shape,retAll=False,noCheck=False):
@ -1171,7 +1175,8 @@ class BaseMulti(Base):
e = cls._entityDef[0]( e = cls._entityDef[0](
solver,partInfo,info.Subname,info.Shape) solver,partInfo,info.Subname,info.Shape)
params = props + [e0,e] 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) h = func(*params,group=solver.group)
if isinstance(h,(list,tuple)): if isinstance(h,(list,tuple)):
ret += list(h) ret += list(h)
@ -1224,16 +1229,19 @@ class BaseMulti(Base):
if i==idx0: if i==idx0:
e0 = cls._entityDef[idx0]( e0 = cls._entityDef[idx0](
solver,partInfo,info.Subname,info.Shape) solver,partInfo,info.Subname,info.Shape)
subname0 = info.SubnameRef
info0 = partInfo info0 = partInfo
else: else:
e = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape) e = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape)
if e0 and e: if e0 and e:
if idx0: if idx0:
params = props + [e,e0] params = props + [e,e0]
solver.system.checkRedundancy(obj,partInfo,info0) solver.system.checkRedundancy(
obj,partInfo,info0,info.SubnameRef,subname0)
else: else:
params = props + [e0,e] 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) h = func(*params,group=solver.group)
if isinstance(h,(list,tuple)): if isinstance(h,(list,tuple)):
ret += list(h) ret += list(h)
@ -1266,7 +1274,8 @@ class BaseCascade(BaseMulti):
params = props + [e1,e2] params = props + [e1,e2]
else: else:
params = props + [e2,e1] 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) h = func(*params,group=solver.group)
if isinstance(h,(list,tuple)): if isinstance(h,(list,tuple)):
ret += list(h) ret += list(h)

View File

@ -10,6 +10,16 @@ from .utils import getElementPos,objName,addIconToFCAD,guilogger as logger
from .proxy import ProxyType from .proxy import ProxyType
from .FCADLogger import FCADLogger 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: class SelectionObserver:
def __init__(self): def __init__(self):
self._attached = False self._attached = False
@ -38,9 +48,13 @@ class SelectionObserver:
res = sobj.Proxy.parent.Object.isElementVisible(sobj.Name) res = sobj.Proxy.parent.Object.isElementVisible(sobj.Name)
if res and vis: if res and vis:
return False return False
if not res and not vis:
return;
sobj.Proxy.parent.Object.setElementVisible(sobj.Name,vis) sobj.Proxy.parent.Object.setElementVisible(sobj.Name,vis)
elif isTypeOf(sobj,AsmConstraint): elif isTypeOf(sobj,AsmConstraint):
vis = [vis] * len(flattenGroup(sobj)) vis = [vis] * len(flattenGroup(sobj))
if sobj.VisibilityList == tuple(vis):
return
sobj.setPropertyStatus('VisibilityList','-Immutable') sobj.setPropertyStatus('VisibilityList','-Immutable')
sobj.VisibilityList = vis sobj.VisibilityList = vis
sobj.setPropertyStatus('VisibilityList','Immutable') sobj.setPropertyStatus('VisibilityList','Immutable')
@ -103,7 +117,9 @@ class SelectionObserver:
hasSelection = FreeCADGui.Selection.hasSelection() hasSelection = FreeCADGui.Selection.hasSelection()
for cmd in self.cmds: for cmd in self.cmds:
cmd.onSelectionChange(hasSelection) cmd.onSelectionChange(hasSelection)
FreeCADGui.updateCommands() update = getattr(FreeCADGui, 'updateCommands', None)
if update:
update()
def addSelection(self,docname,objname,subname,_pos): def addSelection(self,docname,objname,subname,_pos):
self.onChange() self.onChange()
@ -250,6 +266,7 @@ class AsmCmdBase(with_metaclass(AsmCmdManager, object)):
_contextMenuName = 'Assembly' _contextMenuName = 'Assembly'
_accel = None _accel = None
_cmdType = None _cmdType = None
_iconName = None
@classmethod @classmethod
def checkActive(cls): def checkActive(cls):
@ -257,15 +274,18 @@ class AsmCmdBase(with_metaclass(AsmCmdManager, object)):
@classmethod @classmethod
def getIconName(cls): def getIconName(cls):
if cls._iconName:
return addIconToFCAD(cls._iconName) return addIconToFCAD(cls._iconName)
@classmethod @classmethod
def GetResources(cls): def GetResources(cls):
ret = { ret = {
'Pixmap':cls.getIconName(),
'MenuText':cls.getMenuText(), 'MenuText':cls.getMenuText(),
'ToolTip':cls.getToolTip() 'ToolTip':cls.getToolTip()
} }
name = cls.getIconName()
if name:
ret['Pixmap'] = name
if cls._accel: if cls._accel:
ret['Accel'] = cls._accel ret['Accel'] = cls._accel
if cls._cmdType is not None: if cls._cmdType is not None:
@ -853,6 +873,7 @@ class AsmCmdGotoRelation(AsmCmdBase):
_iconName = 'Assembly_GotoRelation.svg' _iconName = 'Assembly_GotoRelation.svg'
_accel = 'A, R' _accel = 'A, R'
_toolbarName = '' _toolbarName = ''
_cmdType = 'NoTransaction'
@classmethod @classmethod
def Activated(cls): def Activated(cls):
@ -950,7 +971,7 @@ class AsmCmdGotoLinked(AsmCmdBase):
@classmethod @classmethod
def IsActive(cls): def IsActive(cls):
return FreeCADGui.isCommandActive('Std_LinkSelectLinked') return _isCommandActive('Std_LinkSelectLinked')
class AsmCmdGotoLinkedFinal(AsmCmdBase): class AsmCmdGotoLinkedFinal(AsmCmdBase):
_id = 23 _id = 23
@ -1008,7 +1029,7 @@ class AsmCmdGotoLinkedFinal(AsmCmdBase):
obj = sels[0].Object.getSubObject(sels[0].SubElementNames[0],1) obj = sels[0].Object.getSubObject(sels[0].SubElementNames[0],1)
if isTypeOf(obj, (AsmElementLink,AsmElement)): if isTypeOf(obj, (AsmElementLink,AsmElement)):
return True return True
return FreeCADGui.isCommandActive('Std_LinkSelectLinkedFinal') return _isCommandActive('Std_LinkSelectLinkedFinal')
class AsmCmdUp(AsmCmdBase): class AsmCmdUp(AsmCmdBase):
_id = 6 _id = 6
@ -1096,3 +1117,38 @@ class AsmCmdMultiply(AsmCmdBase):
@classmethod @classmethod
def onSelectionChange(cls,hasSelection): def onSelectionChange(cls,hasSelection):
cls._active = None if hasSelection else False 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,\ from .gui import AsmCmdManager,AsmCmdGotoRelation,\
AsmCmdGotoLinked, AsmCmdGotoLinkedFinal AsmCmdGotoLinked, AsmCmdGotoLinkedFinal
AsmCmdManager.init() AsmCmdManager.init()
cmdSet = set()
for name,cmds in AsmCmdManager.Toolbars.items(): for name,cmds in AsmCmdManager.Toolbars.items():
cmdSet.update(cmds)
self.appendToolbar(name,[cmd.getName() for cmd in cmds]) self.appendToolbar(name,[cmd.getName() for cmd in cmds])
self.appendToolbar('Assembly3 Navigation', [ self.appendToolbar('Assembly3 Navigation', [
AsmCmdGotoRelation.getName(), AsmCmdGotoLinked.getName(), AsmCmdGotoRelation.getName(), AsmCmdGotoLinked.getName(),
AsmCmdGotoLinkedFinal.getName()]) AsmCmdGotoLinkedFinal.getName()])
for name,cmds in AsmCmdManager.Menus.items(): for name,cmds in AsmCmdManager.Menus.items():
cmdSet.update(cmds)
self.appendMenu(name,[cmd.getName() for cmd in cmds]) self.appendMenu(name,[cmd.getName() for cmd in cmds])
self._observer.setCommands(cmdSet)
self._observer.setCommands(AsmCmdManager.getInfo().Types)
# FreeCADGui.addPreferencePage( # FreeCADGui.addPreferencePage(
# ':/assembly3/ui/assembly3_prefs.ui','Assembly3') # ':/assembly3/ui/assembly3_prefs.ui','Assembly3')
@ -68,7 +66,12 @@ class Assembly3Workbench(FreeCADGui.Workbench):
for name,cmds in menus.items(): for name,cmds in menus.items():
self.appendContextMenu(name,cmds) 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) logger.catch('',self._contextMenu)
FreeCADGui.addWorkbench(Assembly3Workbench) FreeCADGui.addWorkbench(Assembly3Workbench)

View File

@ -15,7 +15,7 @@ class AsmMovingPart(object):
def __init__(self, moveInfo, element, moveElement): def __init__(self, moveInfo, element, moveElement):
hierarchy = moveInfo.HierarchyList hierarchy = moveInfo.HierarchyList
info = moveInfo.ElementInfo 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.assembly = resolveAssembly(info.Parent)
self.viewObject = self.assembly.Object.ViewObject self.viewObject = self.assembly.Object.ViewObject
self.info = info self.info = info
@ -235,6 +235,9 @@ class AsmMovingPart(object):
setPlacement(info.Part,pla) setPlacement(info.Part,pla)
rollback.append((info.PartName,info.Part,info.Placement.copy())) rollback.append((info.PartName,info.Part,info.Placement.copy()))
if QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ShiftModifier:
return
if not gui.AsmCmdManager.AutoRecompute or \ if not gui.AsmCmdManager.AutoRecompute or \
QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ControlModifier: QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ControlModifier:
# AsmCmdManager.AutoRecompute means auto re-solve the system. The # AsmCmdManager.AutoRecompute means auto re-solve the system. The

View File

@ -18,15 +18,12 @@ from .system import System
# plane of the part. # plane of the part.
# EntityMap: string -> entity handle map, for caching # EntityMap: string -> entity handle map, for caching
# Group: transforming entity group handle # 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 # Update: in case the constraint uses the `Multiplication` feature, only the
# first element of all the coplanar edges will be actually constrainted. # first element of all the coplanar edges will be actually constrainted.
# The rest ElementInfo will be stored here for later update by matrix # The rest ElementInfo will be stored here for later update by matrix
# transformation. # transformation.
PartInfo = namedtuple('SolverPartInfo', ('Part','PartName','Placement', PartInfo = namedtuple('SolverPartInfo', ('Part','PartName','Placement',
'Params','Workplane','EntityMap','Group','CstrMap','Update')) 'Params','Workplane','EntityMap','Group','Update'))
class Solver(object): class Solver(object):
def __init__(self,assembly,reportFailed,dragPart,recompute,rollback): def __init__(self,assembly,reportFailed,dragPart,recompute,rollback):
@ -315,7 +312,6 @@ class Solver(object):
Workplane = h, Workplane = h,
EntityMap = {}, EntityMap = {},
Group = group if group else g, Group = group if group else g,
CstrMap = {},
Update = []) Update = [])
self.system.log('{}, {}',partInfo,g) self.system.log('{}, {}',partInfo,g)

View File

@ -115,6 +115,14 @@ class SystemBase(with_metaclass(System, object)):
self.verbose = obj.Verbose self.verbose = obj.Verbose
self.log = logger.info if obj.Verbose else logger.debug 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): class SystemExtension(object):
def __init__(self): def __init__(self):
@ -125,9 +133,17 @@ class SystemExtension(object):
self.firstInfo = None self.firstInfo = None
self.secondInfo = None self.secondInfo = None
self.relax = False 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.cstrObj,self.firstInfo,self.secondInfo=obj,firstInfo,secondInfo
self.firstElement = firstElement
self.secondElement = secondElement
def addSketchPlane(self,*args,**kargs): def addSketchPlane(self,*args,**kargs):
_ = kargs _ = kargs
@ -147,34 +163,121 @@ class SystemExtension(object):
h.append(self.addSameOrientation(n1.entity,n,group=group)) h.append(self.addSameOrientation(n1.entity,n,group=group))
return h 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), msg = '{} between {} and {}'.format(cstrName(self.cstrObj),
self.firstInfo.PartName, self.secondInfo.PartName) firstPart if firstPart else self.firstInfo.PartName,
if warn: secondPart if secondPart else self.secondInfo.PartName)
logger.warn('skip redundant {}', msg, frame=1) if implicit:
logger.msg('redundant implicit constraint {}, {}', msg, count, frame=1)
elif count > limit:
logger.warn('skip redundant {}, {}', msg, count, frame=1)
else: else:
logger.debug('auto relax {}', msg, frame=1) logger.msg('auto relax {}, {}', msg, count, frame=1)
def _countConstraints(self,increment,limit,*names): 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,cstrType,item=None):
first, second = self.firstInfo, self.secondInfo first, second = self.firstInfo, self.secondInfo
if not first or not second: if not first or not second:
return [] 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)
return cstrs
def countConstraints(self,increment,limit,*names): firstElement, secondElement = self.firstElement, self.secondElement
count = len(self._countConstraints(increment,limit,*names))
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: if count>limit:
return -1 return -1
return count return count
@ -189,6 +292,10 @@ class SystemExtension(object):
if count < 0: if count < 0:
return 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: if d or dx or dy:
dx,dy,d = pln2.normal.rot.multVec(FreeCAD.Vector(dx,dy,d)) dx,dy,d = pln2.normal.rot.multVec(FreeCAD.Vector(dx,dy,d))
v = pln2.origin.vector+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 not lockAngle and count==2:
# if there is already some other plane coincident constraint set for # if there is already some other plane coincident constraint set for
# this pair of parts, we reduce this second constraint to either a # this pair of parts, we reduce this second constraint to a 2D
# points horizontal or vertical constraint, i.e. reduce the # PointOnLine. The line is formed by the first part's two elements
# constraining DOF down to 1. # 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 # This 2D PointOnLine effectively reduce the second PlaneCoincidence
# check for differences in x and y components of the points to # constraining DOF down to 1.
# determine whether to use horizontal or vertical constraint. prev = self.coincidences.get(
rot = pln1.normal.pla.Rotation.multiply(pln1.normal.rot) (self.firstInfo.Part, self.secondInfo.Part))
v1 = pln1.normal.pla.multVec(pln1.origin.vector) ln = self.addLineSegment(prev.origin.entity,
v2 = pln2.normal.pla.multVec(v) pln1.origin.entity, group=self.firstInfo.Group)
v1,v2 = project2D(rot, v1, v2) h.append(self.addPointOnLine(
if abs(v1.x-v2.x) < abs(v1.y-v2.y): pln2.origin.entity, ln, pln1.entity, group=group))
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))
return h return h
h.append(self.addPointsCoincident(pln1.origin.entity, e, group=group)) h.append(self.addPointsCoincident(pln1.origin.entity, e, group=group))
@ -225,7 +330,7 @@ class SystemExtension(object):
pln1.normal, pln2.normal, group) pln1.normal, pln2.normal, group)
def addAttachment(self, pln1, pln2, group=0): 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): def addPlaneAlignment(self,d,lockAngle,yaw,pitch,roll,pln1,pln2,group=0):
if not group: if not group:
@ -233,15 +338,12 @@ class SystemExtension(object):
h = [] h = []
if self.relax: if self.relax:
dof = 2 if lockAngle else 1 dof = 2 if lockAngle else 1
cstrs = self._countConstraints(dof,3,'Alignment') cstrs = self._countConstraints(dof,3,'Alignment',item=pln1.entity)
count = len(cstrs) count = len(cstrs)
if count > 3: if count > 3:
return return
if count == 1:
cstrs[0] = pln1.entity
else: else:
count = 0 count = 0
cstrs = None
if d: if d:
h.append(self.addPointPlaneDistance( h.append(self.addPointPlaneDistance(
@ -252,7 +354,7 @@ class SystemExtension(object):
if count<=2: if count<=2:
n1,n2 = pln1.normal,pln2.normal n1,n2 = pln1.normal,pln2.normal
if count==2 and not lockAngle: 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)) h.append(self.addParallel(n2.entity,n1.entity,cstrs[0],group))
else: else:
self.setOrientation(h,lockAngle,yaw,pitch,roll,n1,n2,group) self.setOrientation(h,lockAngle,yaw,pitch,roll,n1,n2,group)

View File

@ -77,7 +77,10 @@ def addIconToFCAD(iconFile,path=None):
def objName(obj): def objName(obj):
try: 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: except Exception:
return '?' return '?'
@ -85,7 +88,7 @@ def isLine(param):
return isinstance(param,(Part.Line,Part.LineSegment)) return isinstance(param,(Part.Line,Part.LineSegment))
def deduceSelectedElement(obj,subname): def deduceSelectedElement(obj,subname):
shape = obj.getSubObject(subname) shape = getElementShape(obj, subname)
if not shape: if not shape:
return return
count = shape.countElement('Face') count = shape.countElement('Face')
@ -207,6 +210,14 @@ def isElement(obj):
def getElement(shape, element): def getElement(shape, element):
res = None 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: try:
res = shape.getElement(element, True) res = shape.getElement(element, True)
except TypeError: except TypeError: