reeCAD源码分析:Assembly4模块
济南友泉软件有限公司
本文主要描述Assembly4的实现原理、相关组件等方面的内容, 并不对使用方法进行阐述,希望对从事FreeCAD研究、国产CAX(CAD/CAE/CAM软件开发的朋友们有帮助。
一、概述
Assembly4模块可实现多个几何体对象的装配功能。需要指出的是,Assembly4装配功能的实现并不是借助于几何约束求解器,而是通过三维局部坐标系变换(也称作基准坐标系,Local Coordinate System, Datum Coordinate System)。
具体来说,Assembly4模块中使用一个App:Part类型的Model表示装配体,可以将多个几何体(App::Part、PartDesign::Body)以App::Link对象的形式添加到装配体Model中。在这过程中,可以通过Expression指定几何体与几何体、几何体与Model的局部坐标系变换关系。然后,Assembly4便可借助于Expresion Engine来完成这种局部坐标系变换,进而实现多个几何体的装配。
二、Assembly4原理
Ref. from Assembly 4 workbench—————————–
The basic principle is the one from assembly without solver: FreeCAD's App::Part container serve as basic building blocs, that are inserted into each other using the App::Link interface introduced with FreeCAD 0.19.
The advantage of App::Link feature is that it's possible to link FreeCAD objects between files, seamlessly: a model in a file will appear in the tree of another model in another file, but the data is not copied across. This allows to create many instances of the same object without any overhead, allowing the creation of large assemblies. In other words, this is identical what other CAD systems do when speaking of assembly.
The particularity of Assembly 4 is that the placement of the inserted (linked) part is done by matching corresponding coordinate systems in the parent assembly and in the linked part: therefore, there must be at least 1 coordinate system in the assembly and at least 1 in the part. There can be many coordinate systems in each, and the user can choose which of these coordinate systems — in the assembly and in the part — shall be used. Under the hood, for those interested in the technical details, the ExpressionEngine of the Placement property of the App::Link is used to superimpose the Placement of two coordinate systems — one in the assembly and one in the inserted part — which fixes all degrees of freedom of the inserted part.
Another particularity of Assembly 4 is that there is no difference between a part and an assembly: it's possible to mix datum objects, 2D sketches, 3D geometries and inserted parts at will. This feature can prove to be extremely powerful once a user gets used to it.
—————————–Ref. from Assembly 4 workbench
三、预备知识:三维空间几何变换
3.1 对偶四元数
四元数(Quaternion)本质上是一种高阶复数,是由实数加上三个虚数单位构成的,它的数学形式为
其中
四元组可以用于表示空间旋转变换,比如绕过原点的单位向量旋转θ角度,四元组表示可表示为
对偶数(Dual number)的概念类似于复数,它的数学形式为
其中r和d分别表示实部和对偶部,ε表示对偶算子。
对偶数的三角函数定义为:
对偶四元数(Dual Quaternions)是实部和对偶部都为四元数的对偶数,又可称为八元数。对偶四元数是对偶数和四元数在多维空间中的有机结合,可以理解为元素为四元数的对偶数,同样可以理解为元素为对偶数的四元数。
较之四元数只能表示3D旋转,对偶数只能表示平移,对偶四元数的优越性体现在它继承了二者的共同特性,从而能统一的表示旋转与平移。常规四元数只能表示空间旋转,而对偶四元数可以表示空间任意旋转和平移的组合。
四、核心组件
Assembly4装配功能的实现主要借助于App::Link、App::Coordinate、App::Expression等组件。
4.1 App::Part
App::Part主要作为几何体容器,Assembly4 Model对象其实就是一个App::Part类型的对象。App::Part通过App::OriginGroupExtension以增加了一个App::Origin对象,正是App::Origin对象定义了一个局部坐标系,App::Part所包含的其他几何体的位置均是相对于这个局部坐标系。
4.2 App::Link
App::Link用于链接外部几何体对象,这些目标几何体对象可以存在于其他FreeCAD文档对象中。
4.3 PartDesign::Body
PartDesign::Body可以看作是一个特征对象容器,用于在二维约束草图的基础之上创建单个三维几何体对象:即在草图的基础之上,通过一系列施加特征以最终生成一个几何体。
4.4 PartDesign::Coordinate
PartDesign::Coordinate就是前面提到的基准坐标系(局部坐标系),几何体对象Placement属性实际上就是相对于这个局部坐标系来定义的。
4.5 App::Expression
App::Expression用于描述几何体之间、几何体与装配体之间对应的基准坐标系的几何变换关系。
五、Assembly4代码分析
Assembly4模块基于描述局部坐标系变换的表达式而非几何约束求解器实现了三维零件的装配功能。
具体来说,在一个名为Model的App::Part对象中,创建多个指向不同零件(App::Part、PartDesign::Body)的连接(App::Link),然后指定零件与Model对象(或其他零件)的几何变换表达式(App::Expression),而这个几何变换表达式实则表示一个局部坐标系变换关系,再此基础之上,利用App::Part提供的局部坐标系变换来完成了多个零件的装配,这就是Assembly4的工作原理。
5.1 创建装配体
装配体对象实际上就是一个App::Part对象,目前同一文档对象中只能有一个名为"Model"的装配体对象。这个装配体对象实际上是承载多个零件连接对象的容器。
newModelCmd.py中定义了创建装配体的newModel命令,
# the real stuff
def Activated(self):
# get the current active document to avoid errors if user changes tab
self.activeDoc = App.activeDocument()
# check whether there is already Model in the document
if not self.checkModel():
# create a group 'Parts' to hold all parts in the assembly document (if any)
# must be done before creating the Asm4 Model
partsGroup = self.activeDoc.getObject('Parts')
if partsGroup is None:
partsGroup = self.activeDoc.addObject( 'App::DocumentObjectGroup', 'Parts' )
# create a new App::Part called 'Model'
model = self.activeDoc.addObject('App::Part','Model')
# set the type as a "proof" that it's an Assembly4 Model
model.Type='Assembly4 Model'
# add an LCS at the root of the Model, and attach it to the 'Origin'
lcs0 = model.newObject('PartDesign::CoordinateSystem','LCS_Origin')
lcs0.Support = [(model.Origin.OriginFeatures[0],'')]
lcs0.MapMode = 'ObjectXY'
lcs0.MapReversed = False
# create a group Constraints to store future solver constraints there
model.newObject('App::DocumentObjectGroup','Constraints')
# create an object Variables to hold variables to be used in this document
model.addObject(Asm4.createVariables())
# create a Configuration property
model.addProperty('App::PropertyEnumeration', 'Configuration', 'Parameters')
model.Configuration = ['Default']
# move existing parts and bodies at the document root to the Parts group
# not nested inside other parts, to keep hierarchy
if partsGroup.TypeId=='App::DocumentObjectGroup':
for obj in self.activeDoc.Objects:
if obj.TypeId in Asm4.containerTypes and obj.Name!='Model' and obj.getParentGeoFeatureGroup() is None:
partsGroup.addObject(obj)
else:
Asm4.warningBox( 'There seems to already be a Parts object, you might get unexpected behaviour' )
# recompute to get rid of the small overlays
model.recompute()
self.activeDoc.recompute()
在newModel::Activated(self)函数中,首先创建了App::Part类型的Model对象,然后将已有的顶级App::Part移入到了名为Parts的App::DocumentObjectGroup对象中。
5.2 创建零件连接
Assembly4中不仅可以通过连接引用本文档中的零件对象,也可以引用当前打开的其他文档中的零件对象。
在InsertLinkCmd.py文件中,insertLink实现了根据用户输入创建引用零件连接的功能。
"""
+-----------------------------------------------+
| the real stuff happens here |
+-----------------------------------------------+
"""
def onCreateLink(self):
# parse the selected items
# TODO : there should only be 1
selectedPart = []
for selected in self.partList.selectedIndexes():
# get the selected part
selectedPart = self.allParts[ selected.row() ]
# get the name of the link (as it should appear in the tree)
linkName = self.linkNameInput.text()
# only create link if there is a Part object and a name
if self.asmPart and selectedPart and linkName:
# create the App::Link with the user-provided name
#createdLink = self.activeDoc.getObject('Model').newObject( 'App::Link', linkName )
createdLink = self.asmPart.newObject( 'App::Link', linkName )
# assign the user-selected selectedPart to it
createdLink.LinkedObject = selectedPart
# If the name was already chosen, and a UID was generated:
if createdLink.Name != linkName:
# we try to set the label to the chosen name
createdLink.Label = linkName
# update the link
createdLink.recompute()
# close the dialog UI...
self.close()
# ... and launch the placement of the inserted part
Gui.Selection.clearSelection()
Gui.Selection.addSelection( self.activeDoc.Name, self.asmPart.Name, createdLink.Name+'.' )
# ... but only if we're in an Asm4 Model
if self.asmPart.Name=='Model':
Gui.runCommand( 'Asm4_placeLink' )
# if still open, close the dialog UI
self.close()
在insertLink::onCreateLink(self)函数中,根据用户选择的零件及通过界面输入的连接名字,创建了App::Link的连接对象用于引用零件。完成连接创建之后,触发了安置零件的命令。
5.3 安置零件
将零件通过App::Link引入到当前Model装配体之后,就可以指定零件与装配体(或零件)之间的局部坐标系变换的表达式了。
placeLinkCmd.py中定义了placeLinkCmd、placeLinkUI等用于完成局部坐标系变换表达式的定义。
"""
+-----------------------------------------------+
| check that all necessary things are selected, |
| populate the expression with the selected |
| elements, put them into the constraint |
| and trigger the recomputation of the part |
+-----------------------------------------------+
"""
def Apply( self ):
# get the instance to attach to:
# it's either the top level assembly or a sister App::Link
if self.parentList.currentText() == 'Parent Assembly':
a_Link = 'Parent Assembly'
a_Part = None
elif self.parentList.currentIndex() > 1:
parent = self.parentTable[ self.parentList.currentIndex() ]
a_Link = parent.Name
a_Part = parent.LinkedObject.Document.Name
else:
a_Link = None
a_Part = None
# the attachment LCS's name in the parent
# check that something is selected in the QlistWidget
if self.attLCSlist.selectedItems():
a_LCS = self.attLCStable[ self.attLCSlist.currentRow() ].Name
else:
a_LCS = None
# the linked App::Part's name
l_Part = self.selectedLink.LinkedObject.Document.Name
# the LCS's name in the linked part to be used for its attachment
# check that something is selected in the QlistWidget
if self.partLCSlist.selectedItems():
#l_LCS = self.partLCSlist.selectedItems()[0].text()
l_LCS = self.partLCStable[ self.partLCSlist.currentRow() ].Name
else:
l_LCS = None
# check that all of them have something in
# constrName has been checked at the beginning
if a_Link and a_LCS and l_Part and l_LCS :
# this is where all the magic is, see:
#
# https://forum.freecadweb.org/viewtopic.php?p=278124#p278124
#
# as of FreeCAD v0.19 the syntax is different:
# https://forum.freecadweb.org/viewtopic.php?f=17&t=38974&p=337784#p337784
#
# expr = ParentLink.Placement * ParentPart#LCS.Placement * constr_LinkName.AttachmentOffset * LinkedPart#LCS.Placement ^ -1'
# expr = LCS_in_the_assembly.Placement * constr_LinkName.AttachmentOffset * LinkedPart#LCS.Placement ^ -1'
expr = Asm4.makeExpressionPart( a_Link, a_Part, a_LCS, l_Part, l_LCS )
# add the Asm4 properties if it's a pure App::Link
Asm4.makeAsmProperties(self.selectedLink)
# store the part where we're attached to in the constraints object
self.selectedLink.AssemblyType = 'Asm4EE'
self.selectedLink.AttachedBy = '#'+l_LCS
self.selectedLink.AttachedTo = a_Link+'#'+a_LCS
# load the expression into the link's Expression Engine
self.selectedLink.setExpression('Placement', expr )
# recompute the object to apply the placement:
self.selectedLink.recompute()
self.parentAssembly.recompute(True)
return True
else:
#FCC.PrintWarning("Problem in selections\\n")
return False
可以看到,在placeLinkUI::apply(self)函数中,根据用户选择,正是通过调用Asm4.makeExpreessionPart()函数构造了l_PART/l_LCS与a_PART/a_LCS之间的局部坐标系变换的表达式。
"""
+-----------------------------------------------+
| populate the ExpressionEngine |
| for a linked App::Part |
+-----------------------------------------------+
"""
def makeExpressionPart( attLink, attDoc, attLCS, linkedDoc, linkLCS ):
# if everything is defined
if attLink and attLCS and linkedDoc and linkLCS:
# this is where all the magic is, see:
#
# https://forum.freecadweb.org/viewtopic.php?p=278124#p278124
#
# as of FreeCAD v0.19 the syntax is different:
# https://forum.freecadweb.org/viewtopic.php?f=17&t=38974&p=337784#p337784
# expr = ParentLink.Placement * ParentPart#LCS.Placement * constr_LinkName.AttachmentOffset * LinkedPart#LCS.Placement ^ -1
# expr = LCS_in_the_assembly.Placement * constr_LinkName.AttachmentOffset * LinkedPart#LCS.Placement ^ -1
# the AttachmentOffset is now a property of the App::Link
# expr = LCS_in_the_assembly.Placement * AttachmentOffset * LinkedPart#LCS.Placement ^ -1
expr = attLCS+'.Placement * AttachmentOffset * '+linkedDoc+'#'+linkLCS+'.Placement ^ -1'
# if we're attached to another sister part (and not the Parent Assembly)
# we need to take into account the Placement of that Part.
if attDoc:
expr = attLink+'.Placement * '+attDoc+'#'+expr
else:
expr = False
return expr
参考
Assembly4 Workbenchhttps://wiki.freecadweb.org/Assembly4_WorkbenchFreeCAD_Assembly4https://github.com/Zolko-123/FreeCAD_Assembly4/tree/a6deaefa10e642ebb2ac0391b1190cd28ed00b24Expressionshttps://wiki.freecadweb.org/Expressions
Quaternion Wikihttps://math.fandom.com/wiki/Quaternion
SongHo OpenGL: Quaternionhttp://www.songho.ca/math/quaternion/quaternion.html
暂无评论内容