FreeCAD源码分析:Assembly4模块

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)本质上是一种高阶复数,是由实数加上三个虚数单位构成的,它的数学形式为

q=w+x\\mathbf{i}+y\\mathbf{j}+z\\mathbf{k}

其中\\mathbf{i}^{2}=\\mathbf{j}^{2}=\\mathbf{k}^{2}=-1

四元组可以用于表示空间旋转变换,比如绕过原点的单位向量\\boldsymbol{\\mathbf{}n}=\\left ( n_{x},n_{y},n_{z} \\right )旋转θ角度,四元组表示可表示为

q=\\cos \\frac{ \\theta }{2} +\\sin\\frac{ \\theta }{2}n_{x}\\boldsymbol{\\mathbf{i}}+\\sin\\frac{ \\theta }{2}n_{y}\\boldsymbol{\\mathbf{j}}+\\sin\\frac{ \\theta }{2} n_{z}\\boldsymbol{\\mathbf{k}}

对偶数(Dual number)的概念类似于复数,它的数学形式为

\\hat{a}=r+d\\varepsilon

其中r和d分别表示实部和对偶部,ε表示对偶算子。

\\varepsilon ^{2}=0

对偶数的三角函数定义为:

sin\\left ( \\hat{a}\\right )=sin\\left ( r\\right )+d\\varepsilon \\cos \\left ( r \\right )

cos\\left ( \\hat{a}\\right )=cos\\left ( r\\right )-d\\varepsilon \\cos \\left ( r \\right )

对偶四元数(Dual Quaternions)是实部和对偶部都为四元数的对偶数,又可称为八元数。对偶四元数是对偶数和四元数在多维空间中的有机结合,可以理解为元素为四元数的对偶数,同样可以理解为元素为对偶数的四元数。

\\hat{q}=\\hat{w}+\\hat{x}\\boldsymbol{\\mathbf{i}}+\\hat{y}\\boldsymbol{\\mathbf{j}}+\\hat{z}\\boldsymbol{\\mathbf{k}}

较之四元数只能表示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

Maths – Quaternions http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/index.htm

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片