Кроме стандартных типов объектов, таких как аннотации, полигональные сетки и детали, 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()
Возможно, вы заметили, что в приведенных выше примерах 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()
При создании скриптовых объектов в 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):
App::DocumentObjectGroupPython
App::FeaturePython
App::GeometryPython
App::LinkElementPython
App::LinkGroupPython
App::LinkPython
App::MaterialObjectPython
App::PlacementPython
Fem::ConstraintPython
Fem::FeaturePython
Fem::FemAnalysisPython
Fem::FemMeshObjectPython
Fem::FemResultObjectPython
Fem::FemSolverObjectPython
Measure::MeasurePython
Mesh::FeaturePython
Part::CustomFeaturePython
Part::FeaturePython
Part::Part2DObjectPython
PartDesign::FeatureAdditivePython
PartDesign::FeatureAddSubPython
PartDesign::FeaturePython
PartDesign::FeatureSubtractivePython
PartDesign::SubShapeBinderPython
Path::FeatureAreaPython
Path::FeatureAreaViewPython
Path::FeatureCompoundPython
Path::FeaturePython
Path::FeatureShapePython
Points::FeaturePython
Sketcher::SketchObjectPython
Spreadsheet::SheetPython
TechDraw::DrawBrokenViewPython
TechDraw::DrawComplexSectionPython
TechDraw::DrawLeaderLinePython
TechDraw::DrawPagePython
TechDraw::DrawRichAnnoPython
TechDraw::DrawTemplatePython
TechDraw::DrawTilePython
TechDraw::DrawTileWeldPython
TechDraw::DrawViewPartPython
TechDraw::DrawViewPython
TechDraw::DrawViewSectionPython
TechDraw::DrawViewSymbolPython
TechDraw::DrawWeldSymbolPython
Полное описание доступно на странице методы FeaturePython.
Свойства - это настоящие строительные блоки объектов FeaturePython. С их помощью ты сможешь взаимодействовать с объектом и изменять его. После создания нового объекта FeaturePython в своём документе ты можешь получить список доступных свойств:
obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython", "Box")
obj.supportedProperties()
Смотри Пользовательские свойства FeaturePython для получения общего представления.
При добавлении новых свойств к пользовательским объектам, позаботьтесь об этом:
<
или >
в описании свойств (это вызывет поломку xml части .FCStd файла).Свойства определены в заголовочном файле 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=[])
type
: Тип свойства.name
: Название свойства.group
: Подраздел свойств (используется в Редакторе свойств).doc
: Всплывающая подсказка (там же).attr
: Атрибут, смотри выше.read_only
: Смотри выше.hidden
: Смотри выше.enum_vals
: Значения перечисления (список строк), актуальны только в том случае, если тип "App::PropertyEnumeration"
.
Список доступных расширений можно получить с помощью grep -RI EXTENSION_PROPERTY_SOURCE_TEMPLATE
в хранилище исходного кода и приведён здесь (для FreeCAD версии 0.21).
Для объектов (objects):
App::GeoFeatureGroupExtensionPython
App::GroupExtensionPython
App::LinkBaseExtensionPython
App::LinkExtensionPython
App::OriginGroupExtensionPython
Part::AttachExtensionPython
TechDraw::CosmeticExtensionPython
Для представления объектов (view objects):
Gui::ViewProviderExtensionPython
Gui::ViewProviderGeoFeatureGroupExtensionPython
Gui::ViewProviderGroupExtensionPython
Gui::ViewProviderOriginGroupExtensionPython
PartGui::ViewProviderAttachExtensionPython
PartGui::ViewProviderSplineExtensionPython
Существуют и другие расширения, но они не работают как таковые:
App::ExtensionPython
TechDrawGui::ViewProviderCosmeticExtensionPython
TechDrawGui::ViewProviderDrawingViewExtensionPython
TechDrawGui::ViewProviderPageExtensionPython
TechDrawGui::ViewProviderTemplateExtensionPython
Дополнительные страницы Вики FreeCADа:
Интересные темы форума про создание объектов с помощью скриптов:
В дополнение к представленным примерам, посмотрите FreeCAD src/Mod/TemplatePyMod/FeaturePython.py, находящийся в папке с исходными кодами FreeCAD.