Scripted objects/ru

Введение

Кроме стандартных типов объектов, таких как аннотации, полигональные сетки и детали, FreeCAD также предлагает удивительную возможность создавать параметрические объекты, 100% написанные на Python, такие объекты называются (Python Features (Функции)). Они ведут себя точно так же, как любой другой объект FreeCAD, автоматически сохраняются и восстанавливаются при сохранении или загрузке файла.

Необходимо понимать одну особенность: по соображениям безопасности файлы FreeCAD никогда не содержат встроенного кода. Код на Python, который вы пишете для создания параметрических объектов, никогда не сохраняется внутри файла. Это означает, что если вы откроете файл, содержащий такой объект, на другом компьютере, если этот код Python недоступен на этом компьютере, объект не будет полностью воссоздан. Если вы распространяете такие объекты среди других пользователей, вам также нужно будет передать свой скрипт на Python, например, в виде Макроса.

Примечание: Имеется возможность упаковки Python кода внутрь FreeCAD файла, используя json сериализацию с помощью App::PropertyPythonObject, но этот код в конечном счёте нельзя будет запустить напрямую, поэтому это мало подходит для наших целей.

Python Features (Функции) следуют тому же правилу что и все остальные FreeCAD features: они разделены на App и GUI части. App часть, Объект Документ (Document Object), определяет геометрию нашего объекта, тогда как его GUI часть, Объект Визуального Представления (View Provider Object) определяет, как объект будет отображаться на экране. Объект View Provider, как и любая другая FreeCAD feature, доступен только при запуске FreeCAD в его собственном графическом интерфейсе. Существует несколько свойств и методов, доступных для создания вашего объекта. Свойства должны принадлежать к любому из предопределённых типов свойств, предлагаемых FreeCAD, и будут отображаться в окне просмотра свойств, чтобы пользователь мог их редактировать. Таким образом, объекты Feature Python полностью параметричны. Вы можете по отдельности задать свойства как Данных (Object), так и свойства Вида (ViewObject).

Простой пример

Следующий пример можно найти в файле src/Mod/TemplatePyMod/FeaturePython.py вместе с несколькими другими примерами:

'''Examples for a feature class and its view provider.'''

import FreeCAD, FreeCADGui
from pivy import coin

class Box:
    def __init__(self, obj):
        '''Add some custom properties to our box feature'''
        obj.addProperty("App::PropertyLength", "Length", "Box", "Length of the box").Length = 1.0
        obj.addProperty("App::PropertyLength", "Width", "Box", "Width of the box").Width = 1.0
        obj.addProperty("App::PropertyLength", "Height", "Box", "Height of the box").Height = 1.0
        obj.Proxy = self

    def onChanged(self, fp, prop):
        '''Do something when a property has changed'''
        FreeCAD.Console.PrintMessage("Change property: " + str(prop) + "\n")

    def execute(self, fp):
        '''Do something when doing a recomputation, this method is mandatory'''
        FreeCAD.Console.PrintMessage("Recompute Python Box feature\n")

class ViewProviderBox:
    def __init__(self, obj):
        '''Set this object to the proxy object of the actual view provider'''
        obj.addProperty("App::PropertyColor","Color", "Box", "Color of the box").Color = (1.0, 0.0, 0.0)
        obj.Proxy = self

    def attach(self, obj):
        '''Setup the scene sub-graph of the view provider, this method is mandatory'''
        self.shaded = coin.SoGroup()
        self.wireframe = coin.SoGroup()
        self.scale = coin.SoScale()
        self.color = coin.SoBaseColor()

        data=coin.SoCube()
        self.shaded.addChild(self.scale)
        self.shaded.addChild(self.color)
        self.shaded.addChild(data)
        obj.addDisplayMode(self.shaded, "Shaded");
        style=coin.SoDrawStyle()
        style.style = coin.SoDrawStyle.LINES
        self.wireframe.addChild(style)
        self.wireframe.addChild(self.scale)
        self.wireframe.addChild(self.color)
        self.wireframe.addChild(data)
        obj.addDisplayMode(self.wireframe, "Wireframe");
        self.onChanged(obj,"Color")

    def updateData(self, fp, prop):
        '''If a property of the handled feature has changed we have the chance to handle this here'''
        # fp is the handled feature, prop is the name of the property that has changed
        l = fp.getPropertyByName("Length")
        w = fp.getPropertyByName("Width")
        h = fp.getPropertyByName("Height")
        self.scale.scaleFactor.setValue(float(l), float(w), float(h))
        pass

    def getDisplayModes(self,obj):
        '''Return a list of display modes.'''
        modes=[]
        modes.append("Shaded")
        modes.append("Wireframe")
        return modes

    def getDefaultDisplayMode(self):
        '''Return the name of the default display mode. It must be defined in getDisplayModes.'''
        return "Shaded"

    def setDisplayMode(self,mode):
        '''Map the display mode defined in attach with those defined in getDisplayModes.\
                Since they have the same names nothing needs to be done. This method is optional'''
        return mode

    def onChanged(self, vp, prop):
        '''Here we can do something when a single property got changed'''
        FreeCAD.Console.PrintMessage("Change property: " + str(prop) + "\n")
        if prop == "Color":
            c = vp.getPropertyByName("Color")
            self.color.rgb.setValue(c[0], c[1], c[2])

    def getIcon(self):
        '''Return the icon in XPM format which will appear in the tree view. This method is\
                optional and if not defined a default icon is shown.'''
        return """
            /* XPM */
            static const char * ViewProviderBox_xpm[] = {
            "16 16 6 1",
            "   c None",
            ".  c #141010",
            "+  c #615BD2",
            "@  c #C39D55",
            "#  c #000000",
            "$  c #57C355",
            "        ........",
            "   ......++..+..",
            "   .@@@@.++..++.",
            "   .@@@@.++..++.",
            "   .@@  .++++++.",
            "  ..@@  .++..++.",
            "###@@@@ .++..++.",
            "##$.@@$#.++++++.",
            "#$#$.$$$........",
            "#$$#######      ",
            "#$$#$$$$$#      ",
            "#$$#$$$$$#      ",
            "#$$#$$$$$#      ",
            " #$#$$$$$#      ",
            "  ##$$$$$#      ",
            "   #######      "};
            """

    def dumps(self):
        '''When saving the document this object gets stored using Python's json module.\
                Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\
                to return a tuple of all serializable objects or None.'''
        return None

    def loads(self,state):
        '''When restoring the serialized object from document we have the chance to set some internals here.\
                Since no data were serialized nothing needs to be done here.'''
        return None

def makeBox():
    FreeCAD.newDocument()
    a=FreeCAD.ActiveDocument.addObject("App::FeaturePython", "Box")
    Box(a)
    ViewProviderBox(a.ViewObject)

makeBox()

Что необходимо отметить

Если Ваш объект полагается на то, что будет пересчитан во время создания, Вы должны сделать это вручную в функции __init__, поскольку пересчёт не вызывается автоматически. Этот пример не требует этого поскольку метод onChanged класса Box имеет тот же эффект, что и функция execute, но пример ниже полагается на то, что будет пересчитан перед тем как будет показан в окне трёхмерного просмотра. В этих примерах это делается вручную с помощью ActiveDocument.recompute(), но в более сложных сценариях Вы должны решить, где пересчитать либо весь документ, или объект FeaturePython.

Этот пример создаёт несколько исключений трасс стека в окне отчётов. Это поскольку метод onChanged класса Box вызывается каждый раз как свойство добавляется в __init__. Когда первое было добавлено, параметры Width и Height и потому попытки доступа к ним безуспешны.

Объяснение __getstate__ и __setstate__, которые были заменены на dumps и loads, находится в теме форума. obj.Proxy.Type is a dict, not a string (obj.Proxy.Type это словарь, а не строка).

obj.addProperty(...) возвращает obj, так что значение свойства может быть задано в той же строке:

obj.addProperty("App::PropertyLength", "Length", "Box", "Length of the box").Length = 1.0

Что эквивалентно:

obj.addProperty("App::PropertyLength", "Length", "Box", "Length of the box")
obj.Length = 1.0

Другой более сложный пример

В этом примере используется модуль Part (Деталь) для создания октаэдра, затем создаётся его представление Coin с помощью Pivy.

Сначала сам Объект Документа (Document object):

import FreeCAD, FreeCADGui, Part
import pivy
from pivy import coin

class Octahedron:
  def __init__(self, obj):
     "Add some custom properties to our box feature"
     obj.addProperty("App::PropertyLength","Length","Octahedron","Length of the octahedron").Length=1.0
     obj.addProperty("App::PropertyLength","Width","Octahedron","Width of the octahedron").Width=1.0
     obj.addProperty("App::PropertyLength","Height","Octahedron", "Height of the octahedron").Height=1.0
     obj.addProperty("Part::PropertyPartShape","Shape","Octahedron", "Shape of the octahedron")
     obj.Proxy = self

  def execute(self, fp):
     # Define six vetices for the shape
     v1 = FreeCAD.Vector(0,0,0)
     v2 = FreeCAD.Vector(fp.Length,0,0)
     v3 = FreeCAD.Vector(0,fp.Width,0)
     v4 = FreeCAD.Vector(fp.Length,fp.Width,0)
     v5 = FreeCAD.Vector(fp.Length/2,fp.Width/2,fp.Height/2)
     v6 = FreeCAD.Vector(fp.Length/2,fp.Width/2,-fp.Height/2)

     # Make the wires/faces
     f1 = self.make_face(v1,v2,v5)
     f2 = self.make_face(v2,v4,v5)
     f3 = self.make_face(v4,v3,v5)
     f4 = self.make_face(v3,v1,v5)
     f5 = self.make_face(v2,v1,v6)
     f6 = self.make_face(v4,v2,v6)
     f7 = self.make_face(v3,v4,v6)
     f8 = self.make_face(v1,v3,v6)
     shell=Part.makeShell([f1,f2,f3,f4,f5,f6,f7,f8])
     solid=Part.makeSolid(shell)
     fp.Shape = solid

  # helper mehod to create the faces
  def make_face(self,v1,v2,v3):
     wire = Part.makePolygon([v1,v2,v3,v1])
     face = Part.Face(wire)
     return face

Теперь , мы обладаем объектом View Provider, ответственным за отображение этого объекта в 3D сцене:

class ViewProviderOctahedron:
  def __init__(self, obj):
     "Set this object to the proxy object of the actual view provider"
     obj.addProperty("App::PropertyColor","Color","Octahedron","Color of the octahedron").Color=(1.0,0.0,0.0)
     obj.Proxy = self

  def attach(self, obj):
     "Setup the scene sub-graph of the view provider, this method is mandatory"
     self.shaded = coin.SoGroup()
     self.wireframe = coin.SoGroup()
     self.scale = coin.SoScale()
     self.color = coin.SoBaseColor()

     self.data=coin.SoCoordinate3()
     self.face=coin.SoIndexedFaceSet()

     self.shaded.addChild(self.scale)
     self.shaded.addChild(self.color)
     self.shaded.addChild(self.data)
     self.shaded.addChild(self.face)
     obj.addDisplayMode(self.shaded,"Shaded");
     style=coin.SoDrawStyle()
     style.style = coin.SoDrawStyle.LINES
     self.wireframe.addChild(style)
     self.wireframe.addChild(self.scale)
     self.wireframe.addChild(self.color)
     self.wireframe.addChild(self.data)
     self.wireframe.addChild(self.face)
     obj.addDisplayMode(self.wireframe,"Wireframe");
     self.onChanged(obj,"Color")

  def updateData(self, fp, prop):
     "If a property of the handled feature has changed we have the chance to handle this here"
     # fp is the handled feature, prop is the name of the property that has changed
     if prop == "Shape":
        s = fp.getPropertyByName("Shape")
        self.data.point.setNum(6)
        cnt=0
        for i in s.Vertexes:
           self.data.point.set1Value(cnt,i.X,i.Y,i.Z)
           cnt=cnt+1

        self.face.coordIndex.set1Value(0,0)
        self.face.coordIndex.set1Value(1,1)
        self.face.coordIndex.set1Value(2,2)
        self.face.coordIndex.set1Value(3,-1)

        self.face.coordIndex.set1Value(4,1)
        self.face.coordIndex.set1Value(5,3)
        self.face.coordIndex.set1Value(6,2)
        self.face.coordIndex.set1Value(7,-1)

        self.face.coordIndex.set1Value(8,3)
        self.face.coordIndex.set1Value(9,4)
        self.face.coordIndex.set1Value(10,2)
        self.face.coordIndex.set1Value(11,-1)

        self.face.coordIndex.set1Value(12,4)
        self.face.coordIndex.set1Value(13,0)
        self.face.coordIndex.set1Value(14,2)
        self.face.coordIndex.set1Value(15,-1)

        self.face.coordIndex.set1Value(16,1)
        self.face.coordIndex.set1Value(17,0)
        self.face.coordIndex.set1Value(18,5)
        self.face.coordIndex.set1Value(19,-1)

        self.face.coordIndex.set1Value(20,3)
        self.face.coordIndex.set1Value(21,1)
        self.face.coordIndex.set1Value(22,5)
        self.face.coordIndex.set1Value(23,-1)

        self.face.coordIndex.set1Value(24,4)
        self.face.coordIndex.set1Value(25,3)
        self.face.coordIndex.set1Value(26,5)
        self.face.coordIndex.set1Value(27,-1)

        self.face.coordIndex.set1Value(28,0)
        self.face.coordIndex.set1Value(29,4)
        self.face.coordIndex.set1Value(30,5)
        self.face.coordIndex.set1Value(31,-1)

  def getDisplayModes(self,obj):
     "Return a list of display modes."
     modes=[]
     modes.append("Shaded")
     modes.append("Wireframe")
     return modes

  def getDefaultDisplayMode(self):
     "Return the name of the default display mode. It must be defined in getDisplayModes."
     return "Shaded"

  def setDisplayMode(self,mode):
     return mode

  def onChanged(self, vp, prop):
     "Here we can do something when a single property got changed"
     FreeCAD.Console.PrintMessage("Change property: " + str(prop) + "\n")
     if prop == "Color":
        c = vp.getPropertyByName("Color")
        self.color.rgb.setValue(c[0],c[1],c[2])

  def getIcon(self):
     return """
        /* XPM */
        static const char * ViewProviderBox_xpm[] = {
        "16 16 6 1",
        "    c None",
        ".   c #141010",
        "+   c #615BD2",
        "@   c #C39D55",
        "#   c #000000",
        "$   c #57C355",
        "        ........",
        "   ......++..+..",
        "   .@@@@.++..++.",
        "   .@@@@.++..++.",
        "   .@@  .++++++.",
        "  ..@@  .++..++.",
        "###@@@@ .++..++.",
        "##$.@@$#.++++++.",
        "#$#$.$$$........",
        "#$$#######      ",
        "#$$#$$$$$#      ",
        "#$$#$$$$$#      ",
        "#$$#$$$$$#      ",
        " #$#$$$$$#      ",
        "  ##$$$$$#      ",
        "   #######      "};
        """

  def dumps(self):
     return None

  def loads(self,state):
     return None

Наконец, как только наш объект и его viewobject определены, нам просто нужно вызвать их (код класса Octahedron и viewprovider может быть скопирован непосредственно в Python консоль FreeCAD):

FreeCAD.newDocument()
a=FreeCAD.ActiveDocument.addObject("App::FeaturePython","Octahedron")
Octahedron(a)
ViewProviderOctahedron(a.ViewObject)

Создание выделяемых объектов

Если вы хотите чтобы ваш объект можно было выбрать, или по крайней мере его часть, щелкнув по нему в окне, вы должны включить его Coin геометрию внутрь узла SoFCSelection. Если ваш объект обладает сложным представлением, с виджетами, аннотациями, и т.д, вам может потребоваться только часть его в SoFCSelection. Всё, что находится в SoFCSelection постоянно сканируется FreeCAD для обнаружения выделения/предварительного отбора, так что имеет смысл попробовать не перегружать его ненужным сканированием.

Как только части scenegraph, которые должны быть выбраны, окажутся внутри узлов SoFCSelection, вам нужно будет предоставить два метода для обработки пути выбора. Путь выделения может иметь форму строки, содержащей имена каждого элемента в пути, или массива объектов scenegraph. Существует два метода: getDetailPath, который преобразует путь из строки в массив объектов scenegraph, и getElementPicked, который берёт элемент, на который был сделан щелчок в scenegraph, и возвращает его строковое имя (обратите внимание, не его строковый путь).

Вот вышеприведённый пример с Молекулой, адаптированный для выбора элементов Молекулы:

class Molecule:
    def __init__(self, obj):
        ''' Add two point properties '''
        obj.addProperty("App::PropertyVector","p1","Line","Start point")
        obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(5,0,0)

        obj.Proxy = self

    def onChanged(self, fp, prop):
        if prop == "p1" or prop == "p2":
            ''' Print the name of the property that has changed '''
            fp.Shape = Part.makeLine(fp.p1,fp.p2)

    def execute(self, fp):
        ''' Print a short message when doing a recomputation, this method is mandatory '''
        fp.Shape = Part.makeLine(fp.p1,fp.p2)

class ViewProviderMolecule:
    def __init__(self, obj):
        ''' Set this object to the proxy object of the actual view provider '''
        obj.Proxy = self
        self.ViewObject = obj
        sep1=coin.SoSeparator()
        sel1 = coin.SoType.fromName('SoFCSelection').createInstance()
        # sel1.policy.setValue(coin.SoSelection.SHIFT)
        sel1.ref()
        sep1.addChild(sel1)
        self.trl1=coin.SoTranslation()
        sel1.addChild(self.trl1)
        sel1.addChild(coin.SoSphere())
        sep2=coin.SoSeparator()
        sel2 = coin.SoType.fromName('SoFCSelection').createInstance()
        sel2.ref()
        sep2.addChild(sel2)
        self.trl2=coin.SoTranslation()
        sel2.addChild(self.trl2)
        sel2.addChild(coin.SoSphere())
        obj.RootNode.addChild(sep1)
        obj.RootNode.addChild(sep2)
        self.updateData(obj.Object, 'p2')
        self.sel1 = sel1
        self.sel2 = sel2

    def getDetailPath(self, subname, path, append):
        vobj = self.ViewObject
        if append:
            path.append(vobj.RootNode)
            path.append(vobj.SwitchNode)

            mode = vobj.SwitchNode.whichChild.getValue()
            if mode >= 0:
                mode = vobj.SwitchNode.getChild(mode)
                path.append(mode)
                sub = Part.splitSubname(subname)[-1]
                if sub == 'Atom1':
                    path.append(self.sel1)
                elif sub == 'Atom2':
                    path.append(self.sel2)
                else:
                    path.append(mode.getChild(0))
        return True

    def getElementPicked(self, pp):
        path = pp.getPath()
        if path.findNode(self.sel1) >= 0:
            return 'Atom1'
        if path.findNode(self.sel2) >= 0:
            return 'Atom2'
        raise NotImplementedError

    def updateData(self, fp, prop):
        "If a property of the handled feature has changed we have the chance to handle this here"
        # fp is the handled feature, prop is the name of the property that has changed
        if prop == "p1":
            p = fp.getPropertyByName("p1")
            self.trl1.translation=(p.x,p.y,p.z)
        elif prop == "p2":
            p = fp.getPropertyByName("p2")
            self.trl2.translation=(p.x,p.y,p.z)

    def dumps(self):
        return None

    def loads(self,state):
        return None

def makeMolecule():
    FreeCAD.newDocument()
    a=FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Molecule")
    Molecule(a)
    ViewProviderMolecule(a.ViewObject)
    FreeCAD.ActiveDocument.recompute()

Работа с простыми формами

Если ваш параметрический объект - это просто геометрическая форма, то вам не нужно использовать view provider объект. Форма будет отображаться стандартными способами представления форм FreeCAD:

import FreeCAD as App
import FreeCADGui
import FreeCAD
import Part
class Line:
    def __init__(self, obj):
        '''"App two point properties" '''
        obj.addProperty("App::PropertyVector","p1","Line","Start point")
        obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(1,0,0)
        obj.Proxy = self

    def execute(self, fp):
        '''"Print a short message when doing a recomputation, this method is mandatory" '''
        fp.Shape = Part.makeLine(fp.p1,fp.p2)

a=FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Line")
Line(a)
a.ViewObject.Proxy=0 # just set it to something different from None (this assignment is needed to run an internal notification)
FreeCAD.ActiveDocument.recompute()

Тот же код с применением ViewProviderLine

import FreeCAD as App
import FreeCADGui
import FreeCAD
import Part

class Line:
    def __init__(self, obj):
         '''"App two point properties" '''
         obj.addProperty("App::PropertyVector","p1","Line","Start point")
         obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(100,0,0)
         obj.Proxy = self

    def execute(self, fp):
        '''"Print a short message when doing a recomputation, this method is mandatory" '''
        fp.Shape = Part.makeLine(fp.p1,fp.p2)

class ViewProviderLine:
   def __init__(self, obj):
      ''' Set this object to the proxy object of the actual view provider '''
      obj.Proxy = self

   def getDefaultDisplayMode(self):
      ''' Return the name of the default display mode. It must be defined in getDisplayModes. '''
      return "Flat Lines"

a=FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Line")
Line(a)
ViewProviderLine(a.ViewObject)
App.ActiveDocument.recompute()

Структура Scenegraph (графа сцены)

Возможно, вы заметили, что в приведенных выше примерах scenegraphs построены несколько по-разному. Некоторые используют obj.addDisplayMode(node, "modename"), в то время как другие используют obj.SwitchNode.getChild(x).addChild(y).

Каждый элемент feature в документе FreeCAD основан на следующей структуре scenegraph:

RootNode
 \- SwitchNode
     \- Shaded
      - Wireframe
      - etc

Параметр SwitchNode отображает только один из своих дочерних элементов, в зависимости от того, какой режим отображения (display mode) выбран в FreeCAD.

Примеры, которые используют addDisplayMode, строят свои scenegraphs исключительно из элементов coin3d scenegraph. В сущности, addDisplayMode добавляет новый дочерний узел к SwitchNode; имя этого узла будет соответствовать режиму отображения, в который он был передан.

Примеры, в которых используется SwitchNode.getChild(x).addChild, также создают часть своей геометрии, используя функции из Верстака Part (Деталь), такие как fp.Shape = Part.makeLine(fp.p1,fp.p2). Это создаёт различные scenegraphs в режиме отображения в SwitchNode; когда мы позже добавим элементы coin3d в scenegraph, нам нужно будет добавить их к существующим scenegraphs в режиме отображения с помощью addChild, а не создавать новый дочерний элемент в SwitchNode.

При использовании addDisplayMode() для добавления геометрии в scenegraph каждый режим отображения (display mode) должен иметь свой собственный узел, который передаётся в addDisplayMode(); не используйте для этого повторно один и тот же узел. Это приведёт к путанице в механизме выбора. Ничего страшного, если под каждым узлом режима отображения (display mode) будут добавлены одинаковые геометрические узлы, просто корень - root каждого режима отображения должен быть разным.

Вот привёденный выше пример molecule, но уже адаптированный для отрисовки только с помощью объектов Coin3D scenegraph вместо использования объектов из Верстака Деталь (Part):

import Part
from pivy import coin

class Molecule:
    def __init__(self, obj):
        ''' Add two point properties '''
        obj.addProperty("App::PropertyVector","p1","Line","Start point")
        obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(5,0,0)

        obj.Proxy = self

    def onChanged(self, fp, prop):
        pass

    def execute(self, fp):
        ''' Print a short message when doing a recomputation, this method is mandatory '''
        pass

class ViewProviderMolecule:
    def __init__(self, obj):
        ''' Set this object to the proxy object of the actual view provider '''
        self.constructed = False
        obj.Proxy = self
        self.ViewObject = obj

    def attach(self, obj):
        material = coin.SoMaterial()
        material.diffuseColor = (1.0, 0.0, 0.0)
        material.emissiveColor = (1.0, 0.0, 0.0)
        drawStyle = coin.SoDrawStyle()
        drawStyle.pointSize.setValue(10)
        drawStyle.style = coin.SoDrawStyle.LINES
        wireframe = coin.SoGroup()
        shaded = coin.SoGroup()
        self.wireframe = wireframe
        self.shaded = shaded

        self.coords = coin.SoCoordinate3()
        self.coords.point.setValues(0, 2, [FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(1, 0, 0)])
        wireframe += self.coords
        wireframe += drawStyle
        wireframe += material
        shaded += self.coords
        shaded += drawStyle
        shaded += material

        g = coin.SoGroup()
        sel1 = coin.SoType.fromName('SoFCSelection').createInstance()
        sel1.style = 'EMISSIVE_DIFFUSE'
        p1 = coin.SoType.fromName('SoIndexedPointSet').createInstance()
        p1.coordIndex.set1Value(0, 0)
        sel1 += p1
        g += sel1
        wireframe += g
        shaded += g

        g = coin.SoGroup()
        sel2 = coin.SoType.fromName('SoFCSelection').createInstance()
        sel2.style = 'EMISSIVE_DIFFUSE'
        p2 = coin.SoType.fromName('SoIndexedPointSet').createInstance()
        p2.coordIndex.set1Value(0, 1)
        sel2 += p2
        g += sel2
        wireframe += g
        shaded += g

        g = coin.SoGroup()
        sel3 = coin.SoType.fromName('SoFCSelection').createInstance()
        sel3.style = 'EMISSIVE_DIFFUSE'
        p3 = coin.SoType.fromName('SoIndexedLineSet').createInstance()
        p3.coordIndex.setValues(0, 2, [0, 1])
        sel3 += p3
        g += sel3
        wireframe += g
        shaded += g

        obj.addDisplayMode(wireframe, 'Wireframe')
        obj.addDisplayMode(shaded, 'Shaded')

        self.sel1 = sel1
        self.sel2 = sel2
        self.sel3 = sel3
        self.constructed = True
        self.updateData(obj.Object, 'p2')

    def getDetailPath(self, subname, path, append):
        vobj = self.ViewObject
        if append:
            path.append(vobj.RootNode)
            path.append(vobj.SwitchNode)

            mode = vobj.SwitchNode.whichChild.getValue()
            FreeCAD.Console.PrintWarning("getDetailPath: mode {} is active\n".format(mode))
            if mode >= 0:
                mode = vobj.SwitchNode.getChild(mode)
                path.append(mode)
                sub = Part.splitSubname(subname)[-1]
                print(sub)
                if sub == 'Atom1':
                    path.append(self.sel1)
                elif sub == 'Atom2':
                    path.append(self.sel2)
                elif sub == 'Line':
                    path.append(self.sel3)
                else:
                    path.append(mode.getChild(0))
        return True

    def getElementPicked(self, pp):
        path = pp.getPath()
        if path.findNode(self.sel1) >= 0:
            return 'Atom1'
        if path.findNode(self.sel2) >= 0:
            return 'Atom2'
        if path.findNode(self.sel3) >= 0:
            return 'Line'
        raise NotImplementedError

    def updateData(self, fp, prop):
        "If a property of the handled feature has changed we have the chance to handle this here"
        # fp is the handled feature, prop is the name of the property that has changed
        if not self.constructed:
            return
        if prop == "p1":
            p = fp.getPropertyByName("p1")
            self.coords.point.set1Value(0, p)
        elif prop == "p2":
            p = fp.getPropertyByName("p2")
            self.coords.point.set1Value(1, p)

    def getDisplayModes(self, obj):
        return ['Wireframe', 'Shaded']

    def getDefaultDisplayMode(self):
        return 'Shaded'

    def setDisplayMode(self, mode):
        return mode

    def dumps(self):
        return None

    def loads(self,state):
        return None

def makeMolecule():
    FreeCAD.newDocument()
    a=FreeCAD.ActiveDocument.addObject("App::FeaturePython","Molecule")
    Molecule(a)
    b=ViewProviderMolecule(a.ViewObject)
    a.touch()
    FreeCAD.ActiveDocument.recompute()
    return a,b

a,b = makeMolecule()

Программируемый объект Part Design (Проектная Деталь)

При создании скриптовых объектов в PartDesign (ПроектнаяДеталь) процесс аналогичен описанным выше скриптовым объектам, но с некоторыми дополнительными соображениями. Мы должны обработать 2 свойства формы, одно для формы, которую мы видим в 3D-виде, а другое для формы, используемой инструментами создания массивов/шаблонов, такими как круговые массивы. Формы (shape) объектов также должны быть объединены с любым материалом, который уже есть в Теле - Body (или вырезан из него в случае Субтрактивных элементов вырезания). И мы должны это учести и немного по-другому подходить к размещению и прикреплению наших объектов.

Функции (features) твёрдотельных объектов ПроектнойДетали, созданные с помощью скриптов, должны основываться либо на PartDesign::FeaturePython, либо на PartDesign::FeatureAdditivePython, либо на PartDesign::FeatureSubtractivePython, а не на Part::FeaturePython. В функциях массивов могут использоваться только аддитивный (выдавливания) и субтрактивный (вырезания) варианты, и если они основаны на Part::FeaturePython, то когда пользователь помещает объект в Тело - Body ПроектнойДетали, оно становится BaseFeature - БазовойФункцией, а не обрабатывается как собственный объект Тело - Body ПроектнойДетали. Примечание: ожидается, что все они будут твёрдотельными, поэтому, если вы создаёте нетвёрдотельный объект, то он должен быть основан на Part::FeaturePython, иначе следующая функция (feature) в дереве попытается объединиться в твёрдое тело, и это не удастся.

Вот простой пример создания примитива Труба (Tube), аналогичного примитиву Труба в Верстаке Деталь (Part), за исключением того, что это будет объект solid feature Верстака ПроектнаяДеталь (PartDesign). Для этого мы создадим 2 отдельных файла: pdtube.FCMacro и pdtube.py. Файл .FCMacro будет запущен пользователем для создания объекта. Файл .py будет содержать определения классов, импортированные с помощью .FCMacro. Причина, по которой мы делаем это таким образом, заключается в сохранении параметрической природы объекта после перезапуска FreeCAD и открытия документа, содержащего одну из наших Труб.

Сначала, файл определения класса:

# -*- coding: utf-8 -*-
#classes should go in pdtube.py
import FreeCAD, FreeCADGui, Part
class PDTube:
    def __init__(self,obj):
        obj.addProperty("App::PropertyLength","Radius1","Tube","Radius1").Radius1 = 5
        obj.addProperty("App::PropertyLength","Radius2","Tube","Radius2").Radius2 = 10
        obj.addProperty("App::PropertyLength","Height","Tube","Height of tube").Height = 10
        self.makeAttachable(obj)
        obj.Proxy = self

    def makeAttachable(self, obj):

        if int(FreeCAD.Version()[1]) >= 19:
            obj.addExtension('Part::AttachExtensionPython')
        else:
            obj.addExtension('Part::AttachExtensionPython', obj)

        obj.setEditorMode('Placement', 0) #non-readonly non-hidden

    def execute(self,fp):
        outer_cylinder = Part.makeCylinder(fp.Radius2, fp.Height)
        inner_cylinder = Part.makeCylinder(fp.Radius1, fp.Height)
        if fp.Radius1 == fp.Radius2: #just make cylinder
            tube_shape = outer_cylinder
        elif fp.Radius1 < fp.Radius2:
            tube_shape = outer_cylinder.cut(inner_cylinder)
        else: #invert rather than error out
            tube_shape = inner_cylinder.cut(outer_cylinder)

        if not hasattr(fp, "positionBySupport"):
            self.makeAttachable(fp)
        fp.positionBySupport()
        tube_shape.Placement = fp.Placement

        #BaseFeature (shape property of type Part::PropertyPartShape) is provided for us
        #with the PartDesign::FeaturePython and related classes, but it might be empty
        #if our object is the first object in the tree.  it's a good idea to check
        #for its existence in case we want to make type Part::FeaturePython, which won't have it

        if hasattr(fp, "BaseFeature") and fp.BaseFeature != None:
            if "Subtractive" in fp.TypeId:
                full_shape = fp.BaseFeature.Shape.cut(tube_shape)
            else:
                full_shape = fp.BaseFeature.Shape.fuse(tube_shape)
            full_shape.transformShape(fp.Placement.inverse().toMatrix(), True) #borrowed from gears workbench
            fp.Shape = full_shape
        else:
            fp.Shape = tube_shape
        if hasattr(fp,"AddSubShape"): #PartDesign::FeatureAdditivePython and
                                      #PartDesign::FeatureSubtractivePython have this
                                      #property but PartDesign::FeaturePython does not
                                      #It is the shape used for copying in pattern features
                                      #for example in making a polar pattern
            tube_shape.transformShape(fp.Placement.inverse().toMatrix(), True)
            fp.AddSubShape = tube_shape

class PDTubeVP:
    def __init__(self, obj):
        '''Set this object to the proxy object of the actual view provider'''
        obj.Proxy = self

    def attach(self,vobj):
        self.vobj = vobj

    def updateData(self, fp, prop):
        '''If a property of the handled feature has changed we have the chance to handle this here'''
        pass

    def getDisplayModes(self,obj):
        '''Return a list of display modes.'''
        modes=[]
        modes.append("Flat Lines")
        modes.append("Shaded")
        modes.append("Wireframe")
        return modes

    def getDefaultDisplayMode(self):
        '''Return the name of the default display mode. It must be defined in getDisplayModes.'''
        return "Flat Lines"

    def setDisplayMode(self,mode):
        '''Map the display mode defined in attach with those defined in getDisplayModes.\
                Since they have the same names nothing needs to be done. This method is optional'''
        return mode

    def onChanged(self, vp, prop):
        '''Here we can do something when a single property got changed'''
        #FreeCAD.Console.PrintMessage("Change property: " + str(prop) + "\n")
        pass

    def getIcon(self):
        '''Return the icon in XPM format which will appear in the tree view. This method is\
                optional and if not defined a default icon is shown.'''
        return """
            /* XPM */
            static const char * ViewProviderBox_xpm[] = {
            "16 16 6 1",
            "   c None",
            ".  c #141010",
            "+  c #615BD2",
            "@  c #C39D55",
            "#  c #000000",
            "$  c #57C355",
            "        ........",
            "   ......++..+..",
            "   .@@@@.++..++.",
            "   .@@@@.++..++.",
            "   .@@  .++++++.",
            "  ..@@  .++..++.",
            "###@@@@ .++..++.",
            "##$.@@$#.++++++.",
            "#$#$.$$$........",
            "#$$#######      ",
            "#$$#$$$$$#      ",
            "#$$#$$$$$#      ",
            "#$$#$$$$$#      ",
            " #$#$$$$$#      ",
            "  ##$$$$$#      ",
            "   #######      "};
            """

    def dumps(self):
        '''When saving the document this object gets stored using Python's json module.\
                Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\
                to return a tuple of all serializable objects or None.'''
        return None

    def loads(self,state):
        '''When restoring the serialized object from document we have the chance to set some internals here.\
                Since no data were serialized nothing needs to be done here.'''
        return None

А теперь файл макро для создания объекта:

# -*- coding: utf-8 -*-

#pdtube.FCMacro
import pdtube
#above line needed if the class definitions above are place in another file: PDTube.py
#this is needed if the tube object is to remain parametric after restarting FreeCAD and loading
#a document containing the object

body = FreeCADGui.ActiveDocument.ActiveView.getActiveObject("pdbody")
if not body:
    FreeCAD.Console.PrintError("No active body.\n")
else:
    from PySide import QtGui
    window = FreeCADGui.getMainWindow()
    items = ["Additive","Subtractive","Neither additive nor subtractive"]
    item,ok =QtGui.QInputDialog.getItem(window,"Select tube type","Select whether you want additive, subtractive, or neither:",items,0,False)
    if ok:
        if item == items[0]:
            className = "PartDesign::FeatureAdditivePython"
        elif item == items[1]:
            className = "PartDesign::FeatureSubtractivePython"
        else:
            className = "PartDesign::FeaturePython" #not usable in pattern features, such as polar pattern

        tube = FreeCAD.ActiveDocument.addObject(className,"Tube")
        pdtube.PDTube(tube)
        pdtube.PDTubeVP(tube.ViewObject)
        body.addObject(tube) #optionally we can also use body.insertObject() for placing at particular place in tree

Доступные типы объектов

Типы объектов, которые вы можете создать с помощью FreeCAD.ActiveDocument.AddObject(), зависят от загруженных модулей. После загрузки всех внутренних верстаков можно получить полный список с помощью FreeCAD.ActiveDocument.SupportedTypes(). Для скриптовых объектов можно использовать только типы объектов, имя которых заканчивается на Python. Они перечислены здесь (для FreeCAD версии 1.0):

Доступные методы

Полное описание доступно на странице методы FeaturePython.

Доступные свойства

Свойства - это настоящие строительные блоки объектов FeaturePython. С их помощью ты сможешь взаимодействовать с объектом и изменять его. После создания нового объекта FeaturePython в своём документе ты можешь получить список доступных свойств:

obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython", "Box")
obj.supportedProperties()

Смотри Пользовательские свойства FeaturePython для получения общего представления.

При добавлении новых свойств к пользовательским объектам, позаботьтесь об этом:

Свойства определены в заголовочном файле PropertyStandard C++.

Атрибуты Свойств

По умолчанию свойства могут быть изменены пользователем, но можно сделать свойства доступными только для чтения, например, если требуется отобразить результат выполнения метода. Также возможно скрыть свойство. Эти атрибуты можно задать с помощью:

obj.setEditorMode("MyPropertyName", mode)

Где mode может иметь следующие значения:

 0 - режим по умолчанию, чтение и запись
 1 - только для чтения
 2 - скрытый
 3 - только для чтения и скрытый

Атрибуты также могут быть установлены с помощью списка строк, например obj.setEditorMode("Placement", ["ReadOnly", "Hidden"]).

Атрибуты, заданные с помощью setEditorMode, могут быть удалены пользователем. Смотри Редактор свойств. Обрати внимание, что свойства, доступные только для чтения, всё ещё можно изменить с помощью Python.

Вы также можете установить эти и другие атрибуты непосредственно с помощью функции addProperty. Атрибуты, установленные с помощью этой функции, не могут быть изменены пользователем. Интересной возможностью является пометка свойства как выходного свойства. Таким образом, FreeCAD не будет помечать объект как затронутый при его изменении (поэтому нет необходимости в повторных вычислениях).

Пример выходного свойства (смотри также https://forum.freecad.org/viewtopic.php?t=24928):

obj.addProperty("App::PropertyString", "MyCustomProperty", "", "", 8)

Ниже перечислены атрибуты, которые можно задать с помощью addProperty. Путём добавления значений можно задать несколько атрибутов.

  0 -- Prop_None, Нет специального атрибута свойства
  1 -- Prop_ReadOnly, Свойство доступно только для чтения в редакторе.
  2 -- Prop_Transient, Свойство не будет сохранено в файле
  4 -- Prop_Hidden, Свойство не появится в редакторе
  8 -- Prop_Output, Изменённое свойство не затрагивает родительский контейнер
 16 -- Prop_NoRecompute, Изменённое свойство не затрагивает свой контейнер для повторного вычисления
 32 -- Prop_NoPersist, Свойство вообще не будет сохранено в файл

Свойства определены в заголовочном файле PropertyContainer C++.

Для Prop_ReadOnly и Prop_Hidden функция addProperty также имеет логические булевы аргументы:

obj.addProperty("App::PropertyString", "MyCustomProperty", "", "", 0, True, True)

Что эквивалентно:

obj.addProperty("App::PropertyString", "MyCustomProperty", "", "", 1+4)

представлено в версии 1.0: Полная сигнатура функции такова:

obj.addProperty(type: string, name: string, group="", doc="", attr=0, read_only=False, hidden=False, enum_vals=[])

Доступные расширения

Список доступных расширений можно получить с помощью grep -RI EXTENSION_PROPERTY_SOURCE_TEMPLATE в хранилище исходного кода и приведён здесь (для FreeCAD версии 0.21).

Для объектов (objects):

Для представления объектов (view objects):

Существуют и другие расширения, но они не работают как таковые:

Дополнительная информация

Дополнительные страницы Вики FreeCADа:

Интересные темы форума про создание объектов с помощью скриптов:

В дополнение к представленным примерам, посмотрите FreeCAD src/Mod/TemplatePyMod/FeaturePython.py, находящийся в папке с исходными кодами FreeCAD.