Scripted objects/pl: Difference between revisions

From FreeCAD Documentation
No edit summary
No edit summary
 
(108 intermediate revisions by 2 users not shown)
Line 1: Line 1:
<languages/>
<languages/>

{{docnav
{{Docnav/pl
|[[PySide|PySide]]
|[[Topological_data_scripting/pl|Skrypty danych topologicznych]]
|[[Embedding FreeCAD|Embedding FreeCAD]]
|[[Scenegraph/pl|Scenogram]]
}}
}}


{{TOCright}}
Besides the standard object types such as annotations, meshes and parts objects, FreeCAD also offers the amazing possibility to build 100% python-scripted objects, called Python Features. Those objects will behave exactly as any other FreeCAD object, and are saved and restored automatically on file save/load.

<span id="Introduction"></span>
==Wprowadzenie==

Oprócz standardowych typów obiektów, takich jak adnotacje, siatki i obiekty części, FreeCAD oferuje również niesamowitą możliwość tworzenia obiektów parametrycznych w 100% napisanych w języku Python, zwanych [[App_FeaturePython/pl|właściwościami Python]]. Obiekty te zachowują się dokładnie tak, jak każdy inny obiekt programu FreeCAD i są zapisywane i przywracane automatycznie podczas zapisywania/wczytywania pliku.

Należy pamiętać o jednej szczególnej kwestii: Ze względów bezpieczeństwa pliki FreeCAD nigdy nie zawierają żadnego osadzonego kodu. Kod Pythona, który piszesz, aby utworzyć obiekty parametryczne, nigdy nie jest zapisywany wewnątrz pliku. Oznacza to, że jeśli otworzysz plik zawierający taki obiekt na innym komputerze, to jeśli ten kod nie będzie dostępny na tym komputerze, obiekt nie zostanie w pełni odtworzony. Jeśli rozpowszechniasz takie obiekty, będziesz musiał rozpowszechnić również swój skrypt Python, na przykład jako [[Macros/pl|makrodefinicję]].

'''Uwaga''': Możliwe jest spakowanie kodu Python wewnątrz pliku FreeCAD za pomocą serializacji json z obiektem App::PropertyPythonObject, ale ten kod nigdy nie może być bezpośrednio uruchomiony i dlatego jest mało przydatny w naszym przypadku.


[[App_FeaturePython|Funkcje Python]] działają według tej samej zasady, co wszystkie funkcje programu FreeCAD. Są podzielone na część aplikacji i część GUI. Część aplikacji, Obiekt Dokumentu, definiuje geometrię naszego obiektu, podczas gdy jego część GUI, Obiekt Dostawcy Widoku, definiuje sposób, w jaki obiekt będzie rysowany na ekranie. Obiekt View Provider, jak każda inna funkcja programu FreeCAD, jest dostępny tylko wtedy, gdy uruchamiamy program FreeCAD w jego własnym GUI. Istnieje kilka właściwości i metod dostępnych w celu zbudowania obiektu. Właściwości muszą należeć do jednego z predefiniowanych typów właściwości, które oferuje FreeCAD, i będą wyświetlane w oknie widoku właściwości, tak aby użytkownik mógł je edytować. W ten sposób obiekty FeaturePython są prawdziwie i całkowicie parametryczne. Możesz zdefiniować właściwości osobno dla obiektu i osobno dla jego obiektu ViewObject.
One particularity must be understood, those objects are saved in FreeCAD FcStd files with python's [https://docs.python.org/3/library/json.html json] module. That module turns a python object as a string, allowing it to be added to the saved file. On load, the json module uses that string to recreate the original object, provided it has access to the source code that created the object. This means that if you save such a custom object and open it on a machine where the python code that generated the object is not present, the object won't be recreated. If you distribute such objects to others, you will need to distribute the python script that created it together.


<span id="Basic_example"></span>
Python Features follow the same rule as all FreeCAD features: they are separated into App and GUI parts. The app part, the Document Object, defines the geometry of our object, while its GUI part, the View Provider Object, defines how the object will be drawn on screen. The View Provider Object, as any other FreeCAD feature, is only available when you run FreeCAD in its own GUI. There are several properties and methods available to build your object. Properties must be of any of the predefined properties types that FreeCAD offers, and will appear in the property view window, so they can be edited by the user. This way, FeaturePython objects are truly and totally parametric. you can define properties for the Object and its ViewObject separately.
==Przykład podstawowy==


Poniższy przykład można znaleźć w pliku [https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/TemplatePyMod/FeaturePython.py FeaturePython.py], wraz z kilkoma innymi przykładami:
'''Hint:''' In former versions we used Python's [http://docs.python.org/release/2.5/lib/module-cPickle.html cPickle] module. However, this module executes arbitrary code and thus causes a security problem. Thus, we moved to Python's json module.


== Basic example ==
The following sample can be found in the [https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/TemplatePyMod/FeaturePython.py src/Mod/TemplatePyMod/FeaturePython.py] file, together with several other examples:
{{Code|code=
{{Code|code=
'''Examples for a feature class and its view provider.'''
'''Examples for a feature class and its view provider.'''
Line 24: Line 33:
def __init__(self, obj):
def __init__(self, obj):
'''Add some custom properties to our box feature'''
'''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", "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", "Width", "Box", "Width of the box").Width = 1.0
obj.addProperty("App::PropertyLength","Height","Box", "Height of the box").Height=1.0
obj.addProperty("App::PropertyLength", "Height", "Box", "Height of the box").Height = 1.0
obj.Proxy = self
obj.Proxy = self

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

def execute(self, fp):
def execute(self, fp):
'''Do something when doing a recomputation, this method is mandatory'''
'''Do something when doing a recomputation, this method is mandatory'''
Line 40: Line 49:
def __init__(self, obj):
def __init__(self, obj):
'''Set this object to the proxy object of the actual view provider'''
'''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.addProperty("App::PropertyColor","Color", "Box", "Color of the box").Color = (1.0, 0.0, 0.0)
obj.Proxy = self
obj.Proxy = self

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

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

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

def getDisplayModes(self,obj):
def getDisplayModes(self,obj):
'''Return a list of display modes.'''
'''Return a list of display modes.'''
Line 79: Line 88:
modes.append("Wireframe")
modes.append("Wireframe")
return modes
return modes

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

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

def onChanged(self, vp, prop):
def onChanged(self, vp, prop):
'''Here we can do something when a single property got changed'''
'''Here we can do something when a single property got changed'''
Line 94: Line 103:
if prop == "Color":
if prop == "Color":
c = vp.getPropertyByName("Color")
c = vp.getPropertyByName("Color")
self.color.rgb.setValue(c[0],c[1],c[2])
self.color.rgb.setValue(c[0], c[1], c[2])

def getIcon(self):
def getIcon(self):
'''Return the icon in XPM format which will appear in the tree view. This method is\
'''Return the icon in XPM format which will appear in the tree view. This method is\
Line 126: Line 135:
" ####### "};
" ####### "};
"""
"""

def __getstate__(self):
def dumps(self):
'''When saving the document this object gets stored using Python's json module.\
'''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\
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.'''
to return a tuple of all serializable objects or None.'''
return None
return None

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



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


makeBox()
makeBox()

}}
}}
=== Things to note ===
If your object relies on being recomputed as soon as it is created, you must do this manually in the {{include|__init__}} function as it is not called automatically. This example does not require it because the {{incode|onChanged}} method of the {{incode|Box}} class has the same effect as the {{include|execute}} function, but the examples below rely on being recomputed before anything is displayed in the 3D view. In the examples, this is done manually with {{incode|ActiveDocument.recompute()}} but in more complex scenarios you need to decide where to recompute either the whole document or the FeaturePython object.


<span id="Things_to_note"></span>
This example produces a number of exception stack traces in the report view window. This is because the {{incode|onChanged}} method of the {{incode|Box}} class is called each time a property is added in {{incode|__init__}}. When the first one is added, the Width and Height properties don't exist yet and so the attempt to access them fails.
===Warto wiedzieć===


Jeśli twój obiekt wymaga ponownego obliczenia zaraz po utworzeniu, musisz to zrobić ręcznie w funkcji {{incode|__init__}}, ponieważ nie jest ona wywoływana automatycznie. W tym przykładzie nie jest to wymagane, ponieważ metoda {{incode|onChanged}} klasy {{incode|Box}} wywołuje ten sam rezultat, co funkcja {{incode|execute}}, ale w poniższych przykładach wymagane jest ponowne obliczenie, zanim cokolwiek zostanie wyświetlone w widoku 3D. W przykładach jest to wykonywane ręcznie za pomocą funkcji {{incode|ActiveDocument.recompute()}}, ale w bardziej złożonych scenariuszach należy zdecydować, w którym miejscu ponownie obliczyć cały dokument lub obiekt FeaturePython.
An explanation of {{incode|__getstate__}} and {{incode|__setstate__}} is in the forum thread [https://forum.freecadweb.org/viewtopic.php?f=18&t=44009&start=10#p377892 obj.Proxy.Type is a dict, not a string].


Ten przykład powoduje powstanie wielu śladów stosu wyjątków w oknie widoku raportu. Dzieje się tak, ponieważ metoda {{incode|onChanged}} klasy {{incode|Box}} jest wywoływana za każdym razem, gdy w {{incode|__init__}} dodawana jest jakaś właściwość. Gdy dodawana jest pierwsza właściwość, właściwości Width i Height jeszcze nie istnieją, więc próba uzyskania do nich dostępu kończy się niepowodzeniem.
== Available properties ==
Properties are the true building stones of FeaturePython objects. Through them, the user will be able to interact and modify your object. After creating a new FeaturePython object in your document ( obj=FreeCAD.ActiveDocument.addObject("App::FeaturePython","Box") ), you can get a list of the available properties by issuing:


Wyjaśnienie działania funkcji {{incode|__getstate__}} i {{incode|__setstate__}} które zostały zastąpione przez {{incode|dumps}} i {{incode|loads}} znajduje się w wątku na forum [https://forum.freecadweb.org/viewtopic.php?f=18&t=44009&start=10#p377892 obj.Proxy.Type jest wartością typu dict, a nie string].
{{Code|code=
obj.supportedProperties()
}}
You will get a list of available properties:
{{Code|code=
App::PropertyBool
App::PropertyBoolList
App::PropertyFloat
App::PropertyFloatList
App::PropertyFloatConstraint
App::PropertyQuantity
App::PropertyQuantityConstraint
App::PropertyAngle
App::PropertyDistance
App::PropertyLength
App::PropertySpeed
App::PropertyAcceleration
App::PropertyForce
App::PropertyPressure
App::PropertyInteger
App::PropertyIntegerConstraint
App::PropertyPercent
App::PropertyEnumeration
App::PropertyIntegerList
App::PropertyIntegerSet
App::PropertyMap
App::PropertyString
App::PropertyUUID
App::PropertyFont
App::PropertyStringList
App::PropertyLink
App::PropertyLinkSub
App::PropertyLinkList
App::PropertyLinkSubList
App::PropertyMatrix
App::PropertyVector
App::PropertyVectorList
App::PropertyPlacement
App::PropertyPlacementLink
App::PropertyPlacementList
App::PropertyColor
App::PropertyColorList
App::PropertyMaterial
App::PropertyPath
App::PropertyFile
App::PropertyFileIncluded
App::PropertyPythonObject
Part::PropertyPartShape
Part::PropertyGeometryList
Part::PropertyShapeHistory
Part::PropertyFilletEdges
Sketcher::PropertyConstraintList
}}

When adding properties to your custom objects, take care of this:
* Do not use characters "<" or ">" in the properties descriptions (that would break the xml pieces in the .fcstd file)
* Properties are stored alphabetically in a .fcstd file. If you have a shape in your properties, any property whose name comes after "Shape" in alphabetic order, will be loaded AFTER the shape, which can cause strange behaviours.


{{incode|obj.addProperty(...)}} zwraca {{incode|obj}}, dzięki czemu wartość właściwości może być ustawiona w tej samej linii:
A complete list of property attributes can be seen in the [https://github.com/FreeCAD/FreeCAD/blob/master/src/App/PropertyStandard.h PropertyStandard C++ header file].
For instance, if you want to allow the user to enter only a limited range of values (e.g. using PropertyIntegerConstraint), in Python you will assign a tuple containing not only the property value, but also the lower and upper limit as well as the stepsize, as below:


{{Code|code=
{{Code|code=
obj.addProperty("App::PropertyLength", "Length", "Box", "Length of the box").Length = 1.0
prop = (value, lower, upper, stepsize)
}}
}}


Co jest równoważne z:
==Property Type==
By default the properties can be updated. It is possible to make the properties read-only, for instance in the case one wants to show the result of a method. It is also possible to hide the property.
The property type can be set using


{{Code|code=
{{Code|code=
obj.addProperty("App::PropertyLength", "Length", "Box", "Length of the box")
obj.setEditorMode("MyPropertyName", mode)
obj.Length = 1.0
}}
}}


<span id="Other_more_complex_example"></span>
where mode is a short int that can be set to:
==Inny, bardziej złożony przykład==
0 -- default mode, read and write
1 -- read-only
2 -- hidden


Ten przykład wykorzystuje środowisko pracy [[Part_Workbench/pl|Część]] do utworzenia ośmiościanu, a następnie tworzy jego reprezentację "coin" za pomocą Pivy.
The EditorModes are not set at FreeCAD file reload. This could to be done by the __setstate__ function. See http://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=10#p108072. By using the setEditorMode the properties are only read only in PropertyEditor. They could still be changed from python. To really make them read only the setting has to be passed directly inside the addProperty function. See http://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=20#p109709 for an example.


Pierwszym z nich jest sam obiekt Dokumentu:
Using the direct setting in the addProperty function, you also have more possibilities. In particular, an interesting one is mark a property as an output property. This way FreeCAD won't mark the feature as touched when changing it (so no need to recompute).

Example of output property (see also https://forum.freecadweb.org/viewtopic.php?t=24928):

{{Code|code=
obj.addProperty("App::PropertyString","MyCustomProperty","","",8)
}}

The property types that can be set at last parameter of the addProperty function are:
0 -- Prop_None, No special property type
1 -- Prop_ReadOnly, Property is read-only in the editor
2 -- Prop_Transient, Property won't be saved to file
4 -- Prop_Hidden, Property won't appear in the editor
8 -- Prop_Output, Modified property doesn't touch its parent container
16 -- Prop_NoRecompute, Modified property doesn't touch its container for recompute


You can find these different property types defined in the [https://github.com/FreeCAD/FreeCAD/blob/master/src/App/PropertyContainer.h source code C++ header for PropertyContainer]

== Other more complex example ==
This example makes use of the [[Part Module|Part Module]] to create an octahedron, then creates its coin representation with pivy.

First is the Document object itself:


{{Code|code=
{{Code|code=
Line 284: Line 207:
v5 = FreeCAD.Vector(fp.Length/2,fp.Width/2,fp.Height/2)
v5 = FreeCAD.Vector(fp.Length/2,fp.Width/2,fp.Height/2)
v6 = 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
# Make the wires/faces
f1 = self.make_face(v1,v2,v5)
f1 = self.make_face(v1,v2,v5)
Line 305: Line 228:
}}
}}


Następnie mamy obiekt dostawcy widoku, odpowiedzialny za wyświetlanie obiektu na scenie 3D:
Then, we have the view provider object, responsible for showing the object in the 3D scene:


{{Code|code=
{{Code|code=
Line 322: Line 245:


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


self.shaded.addChild(self.scale)
self.shaded.addChild(self.scale)
Line 349: Line 272:
self.data.point.set1Value(cnt,i.X,i.Y,i.Z)
self.data.point.set1Value(cnt,i.X,i.Y,i.Z)
cnt=cnt+1
cnt=cnt+1

self.face.coordIndex.set1Value(0,0)
self.face.coordIndex.set1Value(0,0)
self.face.coordIndex.set1Value(1,1)
self.face.coordIndex.set1Value(1,1)
Line 440: Line 363:
"""
"""


def __getstate__(self):
def dumps(self):
return None
return None


def __setstate__(self,state):
def loads(self,state):
return None
return None
}}
}}


Finally, once our object and its viewobject are defined, we just need to call them (The Octahedron class and viewprovider class code could be copied in the FreeCAD python console directly):
Na koniec, gdy nasz obiekt i jego viewobject już zdefiniowane, wystarczy je wywołać ''(kod klasy Octahedron i klasy viewprovider można skopiować bezpośrednio w konsoli Python programu FreeCAD)'':


{{Code|code=
{{Code|code=
Line 456: Line 379:
}}
}}


<span id="Making_objects_selectable"></span>
== Making objects selectable ==
==Robienie obiektów wybieralnymi==
If you want to make your object selectable, or at least part of it, by clicking on it in the viewport, you must include its coin geometry inside a SoFCSelection node. If your object has complex representation, with widgets, annotations, etc, you might want to include only a part of it in a SoFCSelection. Everything that is a SoFCSelection is constantly scanned by FreeCAD to detect selection/preselection, so it makes sense try not to overload it with unneeded scanning.


Jeśli chcesz, aby Twój obiekt był wybieralny, lub przynajmniej jego część, przez kliknięcie na nim w rzutni, musisz umieścić jego geometrię coin w węźle SoFCSelection. Jeśli obiekt ma złożoną reprezentację, z widżetami, adnotacjami itp, możesz chcieć zawrzeć tylko jego część w węźle SoFCSelection. Wszystko, co jest węzłem SoFCSelection, jest stale skanowane przez FreeCAD w celu wykrycia zaznaczenia/poprzedzenia zaznaczenia, więc warto spróbować nie przeciążać go niepotrzebnym skanowaniem.
Once the parts of the scenegraph that are to be selectable are inside SoFCSelection nodes, you then need to provide two methods to handle the selection path. The selection path can take the form of a string giving the names of each element in the path, or of an array of scenegraph objects. The two methods you provide are {{incode|getDetailPath}}, which converts from a string path to an array of scenegraph objects, and {{incode|getElementPicked}}, which takes an element which has been clicked on in the scenegraph and returns its string name (note, not its string path).

Gdy fragmenty scenogramu, które mają być wybieralne, znajdą się w węzłach SoFCSelection, należy dostarczyć dwie metody do obsługi ścieżki wyboru. Ścieżka wyboru może mieć postać łańcucha podającego nazwy poszczególnych elementów ścieżki lub tablicy obiektów scenogramu. Dwie metody, które udostępniasz, to {{incode|getDetailPath}}, która konwertuje ścieżkę łańcuchową na tablicę obiektów scenograficznych, oraz {{incode|getElementPicked}}, która pobiera element kliknięty w scenogramie i zwraca jego nazwę w postaci łańcucha ''(uwaga, nie jego ścieżkę w postaci łańcucha)''.

Oto powyższy przykład cząsteczki, dostosowany do wyboru elementów cząsteczki:


Here is the molecule example above, adapted to make the elements of the molecule selectable:
{{Code|code=
{{Code|code=
class Molecule:
class Molecule:
Line 532: Line 458:
return 'Atom2'
return 'Atom2'
raise NotImplementedError
raise NotImplementedError



def updateData(self, fp, prop):
def updateData(self, fp, prop):
Line 544: Line 469:
self.trl2.translation=(p.x,p.y,p.z)
self.trl2.translation=(p.x,p.y,p.z)


def __getstate__(self):
def dumps(self):
return None
return None


def __setstate__(self,state):
def loads(self,state):
return None
return None


Line 558: Line 483:
}}
}}


<span id="Working_with_simple_shapes"></span>
== Working with simple shapes ==
==Praca z prostymi kształtami==
If your parametric object simply outputs a shape, you don't need to use a view provider object. The shape will be displayed using FreeCAD's standard shape representation:

Jeśli obiekt parametryczny po prostu wyprowadza kształt, nie trzeba używać obiektu dostawcy widoku. Kształt zostanie wyświetlony przy użyciu standardowej reprezentacji kształtu programu FreeCAD:


{{Code|code=
{{Code|code=
Line 583: Line 510:
}}
}}


Same code with use '''ViewProviderLine'''
Ten sam kod z użyciem '''ViewProviderLine'''.


{{Code|code=
{{Code|code=
Line 597: Line 524:
obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(100,0,0)
obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(100,0,0)
obj.Proxy = self
obj.Proxy = self

def execute(self, fp):
def execute(self, fp):
'''"Print a short message when doing a recomputation, this method is mandatory" '''
'''"Print a short message when doing a recomputation, this method is mandatory" '''
Line 617: Line 544:
}}
}}


<span id="Scenegraph_Structure"></span>
==Struktura scenograficzna==

Być może zauważyłeś, że powyższe przykłady konstruują swoje scenagramy w nieco inny sposób. Niektóre używają {{incode|obj.addDisplayMode(node, "modename")}}, podczas gdy inne {{incode|obj.SwitchNode.getChild(x).addChild(y)}}.

Każdy element w dokumencie FreeCAD jest oparty na następującej strukturze scenogramu:

{{Code|code=
RootNode
\- SwitchNode
\- Shaded
- Wireframe
- etc
}}

Węzeł {{incode|SwitchNode}} wyświetla tylko jeden ze swoich potomków, w zależności od tego, jaki tryb wyświetlania został wybrany w programie FreeCAD.

Przykłady, które używają {{incode|addDisplayMode}}, konstruują swoje scenagramy wyłącznie z elementów scenogramu coin3d. Pod przykrywką, {{incode|addDisplayMode}} dodaje nowego potomka do węzła {{incode|SwitchNode}}. Nazwa tego węzła będzie odpowiadać trybowi wyświetlania, który został mu przekazany.

Przykłady, które używają {{incode|SwitchNode.getChild(x).addChild}}, konstruują również część swojej geometrii przy użyciu funkcji ze środowiska pracy Część, takich jak {{incode|1=fp.Shape = Part.makeLine(fp.p1,fp.p2)}}. Dzięki temu pod węzłem {{incode|SwitchNode}} powstają scenografie z różnymi trybami wyświetlania. Gdy później chcemy dodać do nich elementy coin3d, musimy je dodać do istniejących scenogramów z trybami wyświetlania, używając {{incode|addChild}}, a nie tworząc nowego potomka węzła {{incode|SwitchNode}}.

Podczas używania {{incode|addDisplayMode()}} w celu dodania geometrii do scenogramu, każdy tryb wyświetlania powinien mieć swój własny węzeł, który jest przekazywany do {{incode|addDisplayMode()}}; nie używaj do tego ponownie tego samego węzła. Spowoduje to zmylenie mechanizmu selekcji. W porządku, jeśli węzeł każdego trybu wyświetlania ma te same węzły geometrii dodane poniżej, tylko korzeń każdego trybu wyświetlania musi być inny.

Oto powyższy przykład cząsteczki, przystosowany do rysowania tylko obiektami scenograficznymi Coin3D, a nie obiektami z środowiska pracy Część:

{{Code|code=
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()
}}

<span id="Part_Design_scripted_objects"></span>
==Obiekty skryptowe środowiska pracy Projekt Części==

Podczas tworzenia obiektów skryptowych w środowisku pracy Projekt Części proces jest podobny do tworzenia obiektów skryptowych omówionych powyżej, ale z kilkoma dodatkowymi uwagami. Musimy obsługiwać dwie właściwości kształtu, jedną dla kształtu, który widzimy w oknie widoku 3D, a drugą dla kształtu używanego przez narzędzia wzorca, takie jak cechy wzoru biegunowego. Kształty obiektów muszą również zostać połączone z istniejącym materiałem już znajdującym się w zawartości ''(lub wycięte z niego w przypadku cech typu Subtractive)''. Musimy także w nieco inny sposób uwzględniać umieszczanie i mocowanie naszych obiektów.

Opisane w skryptach funkcje obiektów bryłowych Projekt Części powinny opierać się na: PartDesign::FeaturePython, PartDesign::FeatureAdditivePython lub PartDesign::FeatureSubtractivePython, a nie na Part::FeaturePython. Tylko warianty Additive i Subtractive mogą być używane we wzorcach cech, a jeśli bazują na Part::FeaturePython, gdy użytkownik upuści obiekt do bryły Projekt Części, staje się on BaseFeature, zamiast być traktowany przez bryłę jako natywny obiekt Projekt Części. '''Uwaga''': wszystkie te obiekty mają być bryłami, więc jeśli tworzysz obiekt nie będący bryłą, powinien on być oparty na Part::FeaturePython, w przeciwnym razie następny obiekt w drzewie będzie próbował połączyć się z nim jako bryłą i to się nie uda.

Oto prosty przykład tworzenia elementu pierwotnego rury, podobnego do elementu pierwotnego rury w środowisku pracy Część, z tą różnicą, że ten element będzie obiektem bryłowym środowiska pracy Projekt Części. W tym celu utworzymy dwa osobne pliki: pdtube.FCMacro i pdtube.py. Plik .FCMacro będzie wykonywany przez użytkownika w celu utworzenia obiektu. Plik .py będzie zawierał definicje klas zaimportowane przez plik .FCMacro. Powodem takiego postępowania jest zachowanie parametrycznego charakteru obiektu po ponownym uruchomieniu programu FreeCAD i otwarciu dokumentu zawierającego jedną z naszych rur.

Po pierwsze, plik definicji klasy:

{{Code|code=
# -*- 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
}}

A teraz plik makrodefinicji do utworzenia obiektu:

{{Code|code=
# -*- 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
}}

<span id="Available_object_types"></span>
==Dostępne typy obiektów==

Listę wszystkich typów obiektów, które można utworzyć za pomocą {{incode|FreeCAD.ActiveDocument.addObject()}} można uzyskać za pomocą {{incode|FreeCAD.ActiveDocument.supportedTypes()}}. Tylko typy obiektów o nazwie kończącej się na {{incode|Python}} mogą być używane dla obiektów skryptowych. Są one wymienione tutaj (dla FreeCAD v0.21):
* {{incode|App::DocumentObjectGroupPython}}
* {{incode|App::FeaturePython}}
* {{incode|App::GeometryPython}}
* {{incode|App::LinkElementPython}}
* {{incode|App::LinkGroupPython}}
* {{incode|App::LinkPython}}
* {{incode|App::MaterialObjectPython}}
* {{incode|App::PlacementPython}}
* {{incode|Part::CustomFeaturePython}}
* {{incode|Part::FeaturePython}}
* {{incode|Part::Part2DObjectPython}}
* {{incode|Path::FeatureAreaPython}}
* {{incode|Path::FeatureAreaViewPython}}
* {{incode|Path::FeatureCompoundPython}}
* {{incode|Path::FeaturePython}}
* {{incode|Path::FeatureShapePython}}
* {{incode|Sketcher::SketchObjectPython}}
* {{incode|TechDraw::DrawComplexSectionPython}}
* {{incode|TechDraw::DrawLeaderLinePython}}
* {{incode|TechDraw::DrawPagePython}}
* {{incode|TechDraw::DrawRichAnnoPython}}
* {{incode|TechDraw::DrawTemplatePython}}
* {{incode|TechDraw::DrawTilePython}}
* {{incode|TechDraw::DrawTileWeldPython}}
* {{incode|TechDraw::DrawViewPartPython}}
* {{incode|TechDraw::DrawViewPython}}
* {{incode|TechDraw::DrawViewSectionPython}}
* {{incode|TechDraw::DrawViewSymbolPython}}
* {{incode|TechDraw::DrawWeldSymbolPython}}

<span id="Available_methods"></span>
==Dostępne metody==

Zobacz stronę [[FeaturePython_methods|Metody FeaturePython]], aby zapoznać się z pełnym opisem.

<span id="Available_properties"></span>
==Dostępne własności==

Właściwości są prawdziwymi kamieniami węgielnymi obiektów FeaturePython. Dzięki nim użytkownik będzie mógł wchodzić w interakcje z obiektem i modyfikować go. Po utworzeniu nowego obiektu FeaturePython w swoim dokumencie ''(obj=FreeCAD.ActiveDocument.addObject("App::FeaturePython", "Box") )'' możesz uzyskać listę dostępnych właściwości, wydając polecenie:

{{Code|code=
obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython", "Box")
obj.supportedProperties()
}}

Zobacz [[FeaturePython_Custom_Properties/pl|Właściwości niestandardowe funkcji Python]], aby uzyskać więcej informacji.

Podczas dodawania właściwości do obiektów niestandardowych należy zwrócić uwagę na następujące kwestie:
* Nie używaj znaków {{Incode|<}} lub {{Incode|>}} w opisach właściwości ''(spowodowałoby to przerwanie fragmentów xml w pliku .FCStd)''
* Właściwości są przechowywane w pliku .FCStd w porządku alfabetycznym. Jeśli we właściwościach znajduje się kształt, każda właściwość, której nazwa w porządku alfabetycznym znajduje się za "Kształtem", zostanie wczytana PRZED kształtem, co może powodować dziwne zachowania.

Typy właściwości są zdefiniowane w [https://github.com/FreeCAD/FreeCAD/blob/master/src/App/PropertyStandard.h kodzie źródłowym nagłówka C++ dla PropertyStandard].

<span id="Property_types"></span>
===Typy właściwości===

Domyślnie właściwości mogą być aktualizowane. Możliwe jest nadanie właściwościom statusu tylko do odczytu, na przykład w sytuacji, gdy chcemy pokazać wynik działania metody. Możliwe jest także ukrycie właściwości. Typ właściwości można ustawić za pomocą:

{{Code|code=
obj.setEditorMode("MyPropertyName", mode)
}}

gdzie ''mode'' jest krótką liczbą całkowitą, której można nadać następujące wartości:
0 -- tryb domyślny, odczyt i zapis
1 -- tylko do odczytu
2 -- ukryty

Tryb można również ustawić za pomocą listy ciągów znaków, np. {{incode|obj.setEditorMode("Placement", ["ReadOnly", "Hidden"])}}.

Tryby edytora nie są ustawiane przy ponownym wczytywaniu pliku FreeCAD. Można to zrobić za pomocą funkcji {{Incode|loads}}. Zobacz http://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=10#p108072. Dzięki użyciu {{Incode|setEditorMode}} właściwości są tylko do odczytu w [[Property_editor/pl|Edytorze właściwości]]. Nadal można je zmieniać z poziomu Pythona. Aby naprawdę uczynić je tylko do odczytu, ustawienie musi być przekazane bezpośrednio wewnątrz funkcji {{Incode|addProperty}}. Przykład można znaleźć na stronie http://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=20#p109709.

Korzystając z bezpośredniego ustawienia w funkcji {{Incode|addProperty}}, masz też więcej możliwości. W szczególności interesującą możliwością jest oznaczenie właściwości jako właściwości wyjściowej. W ten sposób FreeCAD nie będzie oznaczał właściwości jako dotkniętej podczas jej zmiany ''(nie ma więc potrzeby ponownego obliczania)''.

Przykład właściwości wyjściowej ''(zob. też https://forum.freecadweb.org/viewtopic.php?t=24928)'':

{{Code|code=
obj.addProperty("App::PropertyString", "MyCustomProperty", "", "", 8)
}}

Typy właściwości, które można ustawić w ostatnim parametrze funkcji addProperty, są następujące:
0 -- Prop_None, brak specjalnego typu właściwości
1 -- Prop_ReadOnly, właściwość jest tylko do odczytu w edytorze
2 -- Prop_Transient, właściwość nie zostanie zapisana do pliku
4 -- Prop_Hidden, właściwość nie będzie widoczna w edytorze
8 -- Prop_Output, Zmodyfikowana właściwość nie dotyka swojego kontenera nadrzędnego
16 -- Prop_NoRecompute, Zmodyfikowana właściwość nie dotyka swojego kontenera do rekompilacji
32 -- Prop_NoPersist, właściwość nie zostanie zapisana do pliku

Typy właściwości są zdefiniowane w [https://github.com/FreeCAD/FreeCAD/blob/master/src/App/PropertyContainer.h kodzie źródłowym nagłówka C++ dla PropertyContainer].

<span id="Available_extensions"></span>
==Dostępne rozszerzenia==

Listę dostępnych rozszerzeń można uzyskać za pomocą {{incode|grep -RI EXTENSION_PROPERTY_SOURCE_TEMPLATE}} w repozytorium kodu źródłowego i jest podana tutaj (dla FreeCAD v0.21).

W przypadku obiektów:
* {{incode|App::GeoFeatureGroupExtensionPython}}
* {{incode|App::GroupExtensionPython}}
* {{incode|App::LinkBaseExtensionPython}}
* {{incode|App::LinkExtensionPython}}
* {{incode|App::OriginGroupExtensionPython}}
* {{incode|Part::AttachExtensionPython}}
* {{incode|TechDraw::CosmeticExtensionPython}}

W przypadku obiektów widoku:
* {{incode|Gui::ViewProviderExtensionPython}}
* {{incode|Gui::ViewProviderGeoFeatureGroupExtensionPython}}
* {{incode|Gui::ViewProviderGroupExtensionPython}}
* {{incode|Gui::ViewProviderOriginGroupExtensionPython}}
* {{incode|PartGui::ViewProviderAttachExtensionPython}}
* {{incode|PartGui::ViewProviderSplineExtensionPython}}

Istnieją inne rozszerzenia, ale nie działają one w obecnej formie:
* {{incode|App::ExtensionPython}}
* {{incode|TechDrawGui::ViewProviderCosmeticExtensionPython}}
* {{incode|TechDrawGui::ViewProviderDrawingViewExtensionPython}}
* {{incode|TechDrawGui::ViewProviderPageExtensionPython}}
* {{incode|TechDrawGui::ViewProviderTemplateExtensionPython}}

<span id="Further_information"></span>
==Informacje dodatkowe==
==Informacje dodatkowe==


Dodatkowe strony:
Additional pages:
* [[Scripted_objects_saving_attributes|Scripted objects saving attributes]]
* [[Scripted_objects_saving_attributes/pl|Obiekty tworzone skryptami, zapis atrybutów]]
* [[Scripted_objects_migration|Scripted objects migration]]
* [[Scripted_objects_migration/pl|Obiekty tworzone skryptami, migracja]]
* [[Scripted_objects_with_attachment/pl|Obiekty tworzone skryptami, z załącznikiem]]
* [[Scripted objects with attachment|Scripted objects with attachment]]
* [[Viewprovider|Viewproviders]]
* [[Viewprovider/pl|Dostawca widoku]]

Ciekawe wątki na forum dotyczące obiektów tworzonych skryptami:


Interesting forum threads about scripted objects:
* [http://forum.freecadweb.org/viewtopic.php?f=22&t=13740 Python object attributes lost at load]
* [http://forum.freecadweb.org/viewtopic.php?f=22&t=13740 Python object attributes lost at load]
* [http://forum.freecadweb.org/viewtopic.php?t=12139 New FeaturePython is grey]
* [http://forum.freecadweb.org/viewtopic.php?t=12139 New FeaturePython is grey]
* [https://forum.freecadweb.org/viewtopic.php?f=18&t=44009 Explanation on __getstate__ and __setstate__], [https://docs.python.org/3/library/pickle.html#object.__getstate__ official documentation]
* [https://forum.freecadweb.org/viewtopic.php?f=18&t=44009 Explanation on dumps and loads], [https://docs.python.org/3/library/pickle.html#object.__getstate__ official documentation]
* [https://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=20#p109709 Eigenmode frequency always 0?]
* [https://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=20#p109709 Eigenmode frequency always 0?]
* [https://forum.freecadweb.org/viewtopic.php?f=22&t=21330 how to implement python feature's setEdit properly?]
* [https://forum.freecadweb.org/viewtopic.php?f=22&t=21330 how to implement python feature's setEdit properly?]


In addition to the examples presented here have a look at FreeCAD source code [https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/TemplatePyMod/FeaturePython.py src/Mod/TemplatePyMod/FeaturePython.py] for more examples.
Oprócz przykładów przedstawionych tutaj więcej przykładów można znaleźć w kodzie źródłowym programu FreeCAD [https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/TemplatePyMod/FeaturePython.py src/Mod/TemplatePyMod/FeaturePython.py].



{{Docnav/pl
{{docnav
|[[Topological_data_scripting/pl|Skrypty danych topologicznych]]
|[[PySide|PySide]]
|[[Scenegraph/pl|Scenogram]]
|[[Embedding FreeCAD|Embedding FreeCAD]]
}}
}}


{{Powerdocnavi{{#translation:}}}}
{{Powerdocnavi{{#translation:}}}}
[[Category:Developer Documentation{{#translation:}}]]
[[Category:Python Code{{#translation:}}]]
[[Category:Python Code{{#translation:}}]]
{{clear}}

Latest revision as of 10:55, 2 February 2024

Wprowadzenie

Oprócz standardowych typów obiektów, takich jak adnotacje, siatki i obiekty części, FreeCAD oferuje również niesamowitą możliwość tworzenia obiektów parametrycznych w 100% napisanych w języku Python, zwanych właściwościami Python. Obiekty te zachowują się dokładnie tak, jak każdy inny obiekt programu FreeCAD i są zapisywane i przywracane automatycznie podczas zapisywania/wczytywania pliku.

Należy pamiętać o jednej szczególnej kwestii: Ze względów bezpieczeństwa pliki FreeCAD nigdy nie zawierają żadnego osadzonego kodu. Kod Pythona, który piszesz, aby utworzyć obiekty parametryczne, nigdy nie jest zapisywany wewnątrz pliku. Oznacza to, że jeśli otworzysz plik zawierający taki obiekt na innym komputerze, to jeśli ten kod nie będzie dostępny na tym komputerze, obiekt nie zostanie w pełni odtworzony. Jeśli rozpowszechniasz takie obiekty, będziesz musiał rozpowszechnić również swój skrypt Python, na przykład jako makrodefinicję.

Uwaga: Możliwe jest spakowanie kodu Python wewnątrz pliku FreeCAD za pomocą serializacji json z obiektem App::PropertyPythonObject, ale ten kod nigdy nie może być bezpośrednio uruchomiony i dlatego jest mało przydatny w naszym przypadku.

Funkcje Python działają według tej samej zasady, co wszystkie funkcje programu FreeCAD. Są podzielone na część aplikacji i część GUI. Część aplikacji, Obiekt Dokumentu, definiuje geometrię naszego obiektu, podczas gdy jego część GUI, Obiekt Dostawcy Widoku, definiuje sposób, w jaki obiekt będzie rysowany na ekranie. Obiekt View Provider, jak każda inna funkcja programu FreeCAD, jest dostępny tylko wtedy, gdy uruchamiamy program FreeCAD w jego własnym GUI. Istnieje kilka właściwości i metod dostępnych w celu zbudowania obiektu. Właściwości muszą należeć do jednego z predefiniowanych typów właściwości, które oferuje FreeCAD, i będą wyświetlane w oknie widoku właściwości, tak aby użytkownik mógł je edytować. W ten sposób obiekty FeaturePython są prawdziwie i całkowicie parametryczne. Możesz zdefiniować właściwości osobno dla obiektu i osobno dla jego obiektu ViewObject.

Przykład podstawowy

Poniższy przykład można znaleźć w pliku FeaturePython.py, wraz z kilkoma innymi przykładami:

'''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()

Warto wiedzieć

Jeśli twój obiekt wymaga ponownego obliczenia zaraz po utworzeniu, musisz to zrobić ręcznie w funkcji __init__, ponieważ nie jest ona wywoływana automatycznie. W tym przykładzie nie jest to wymagane, ponieważ metoda onChanged klasy Box wywołuje ten sam rezultat, co funkcja execute, ale w poniższych przykładach wymagane jest ponowne obliczenie, zanim cokolwiek zostanie wyświetlone w widoku 3D. W przykładach jest to wykonywane ręcznie za pomocą funkcji ActiveDocument.recompute(), ale w bardziej złożonych scenariuszach należy zdecydować, w którym miejscu ponownie obliczyć cały dokument lub obiekt FeaturePython.

Ten przykład powoduje powstanie wielu śladów stosu wyjątków w oknie widoku raportu. Dzieje się tak, ponieważ metoda onChanged klasy Box jest wywoływana za każdym razem, gdy w __init__ dodawana jest jakaś właściwość. Gdy dodawana jest pierwsza właściwość, właściwości Width i Height jeszcze nie istnieją, więc próba uzyskania do nich dostępu kończy się niepowodzeniem.

Wyjaśnienie działania funkcji __getstate__ i __setstate__ które zostały zastąpione przez dumps i loads znajduje się w wątku na forum obj.Proxy.Type jest wartością typu dict, a nie string.

obj.addProperty(...) zwraca obj, dzięki czemu wartość właściwości może być ustawiona w tej samej linii:

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

Co jest równoważne z:

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

Inny, bardziej złożony przykład

Ten przykład wykorzystuje środowisko pracy Część do utworzenia ośmiościanu, a następnie tworzy jego reprezentację "coin" za pomocą Pivy.

Pierwszym z nich jest sam obiekt Dokumentu:

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

Następnie mamy obiekt dostawcy widoku, odpowiedzialny za wyświetlanie obiektu na scenie 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

Na koniec, gdy nasz obiekt i jego viewobject są już zdefiniowane, wystarczy je wywołać (kod klasy Octahedron i klasy viewprovider można skopiować bezpośrednio w konsoli Python programu FreeCAD):

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

Robienie obiektów wybieralnymi

Jeśli chcesz, aby Twój obiekt był wybieralny, lub przynajmniej jego część, przez kliknięcie na nim w rzutni, musisz umieścić jego geometrię coin w węźle SoFCSelection. Jeśli obiekt ma złożoną reprezentację, z widżetami, adnotacjami itp, możesz chcieć zawrzeć tylko jego część w węźle SoFCSelection. Wszystko, co jest węzłem SoFCSelection, jest stale skanowane przez FreeCAD w celu wykrycia zaznaczenia/poprzedzenia zaznaczenia, więc warto spróbować nie przeciążać go niepotrzebnym skanowaniem.

Gdy fragmenty scenogramu, które mają być wybieralne, znajdą się w węzłach SoFCSelection, należy dostarczyć dwie metody do obsługi ścieżki wyboru. Ścieżka wyboru może mieć postać łańcucha podającego nazwy poszczególnych elementów ścieżki lub tablicy obiektów scenogramu. Dwie metody, które udostępniasz, to getDetailPath, która konwertuje ścieżkę łańcuchową na tablicę obiektów scenograficznych, oraz getElementPicked, która pobiera element kliknięty w scenogramie i zwraca jego nazwę w postaci łańcucha (uwaga, nie jego ścieżkę w postaci łańcucha).

Oto powyższy przykład cząsteczki, dostosowany do wyboru elementów cząsteczki:

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()

Praca z prostymi kształtami

Jeśli obiekt parametryczny po prostu wyprowadza kształt, nie trzeba używać obiektu dostawcy widoku. Kształt zostanie wyświetlony przy użyciu standardowej reprezentacji kształtu programu 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()

Ten sam kod z użyciem 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()

Struktura scenograficzna

Być może zauważyłeś, że powyższe przykłady konstruują swoje scenagramy w nieco inny sposób. Niektóre używają obj.addDisplayMode(node, "modename"), podczas gdy inne obj.SwitchNode.getChild(x).addChild(y).

Każdy element w dokumencie FreeCAD jest oparty na następującej strukturze scenogramu:

RootNode
 \- SwitchNode
     \- Shaded
      - Wireframe
      - etc

Węzeł SwitchNode wyświetla tylko jeden ze swoich potomków, w zależności od tego, jaki tryb wyświetlania został wybrany w programie FreeCAD.

Przykłady, które używają addDisplayMode, konstruują swoje scenagramy wyłącznie z elementów scenogramu coin3d. Pod przykrywką, addDisplayMode dodaje nowego potomka do węzła SwitchNode. Nazwa tego węzła będzie odpowiadać trybowi wyświetlania, który został mu przekazany.

Przykłady, które używają SwitchNode.getChild(x).addChild, konstruują również część swojej geometrii przy użyciu funkcji ze środowiska pracy Część, takich jak fp.Shape = Part.makeLine(fp.p1,fp.p2). Dzięki temu pod węzłem SwitchNode powstają scenografie z różnymi trybami wyświetlania. Gdy później chcemy dodać do nich elementy coin3d, musimy je dodać do istniejących scenogramów z trybami wyświetlania, używając addChild, a nie tworząc nowego potomka węzła SwitchNode.

Podczas używania addDisplayMode() w celu dodania geometrii do scenogramu, każdy tryb wyświetlania powinien mieć swój własny węzeł, który jest przekazywany do addDisplayMode(); nie używaj do tego ponownie tego samego węzła. Spowoduje to zmylenie mechanizmu selekcji. W porządku, jeśli węzeł każdego trybu wyświetlania ma te same węzły geometrii dodane poniżej, tylko korzeń każdego trybu wyświetlania musi być inny.

Oto powyższy przykład cząsteczki, przystosowany do rysowania tylko obiektami scenograficznymi Coin3D, a nie obiektami z środowiska pracy Część:

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()

Obiekty skryptowe środowiska pracy Projekt Części

Podczas tworzenia obiektów skryptowych w środowisku pracy Projekt Części proces jest podobny do tworzenia obiektów skryptowych omówionych powyżej, ale z kilkoma dodatkowymi uwagami. Musimy obsługiwać dwie właściwości kształtu, jedną dla kształtu, który widzimy w oknie widoku 3D, a drugą dla kształtu używanego przez narzędzia wzorca, takie jak cechy wzoru biegunowego. Kształty obiektów muszą również zostać połączone z istniejącym materiałem już znajdującym się w zawartości (lub wycięte z niego w przypadku cech typu Subtractive). Musimy także w nieco inny sposób uwzględniać umieszczanie i mocowanie naszych obiektów.

Opisane w skryptach funkcje obiektów bryłowych Projekt Części powinny opierać się na: PartDesign::FeaturePython, PartDesign::FeatureAdditivePython lub PartDesign::FeatureSubtractivePython, a nie na Part::FeaturePython. Tylko warianty Additive i Subtractive mogą być używane we wzorcach cech, a jeśli bazują na Part::FeaturePython, gdy użytkownik upuści obiekt do bryły Projekt Części, staje się on BaseFeature, zamiast być traktowany przez bryłę jako natywny obiekt Projekt Części. Uwaga: wszystkie te obiekty mają być bryłami, więc jeśli tworzysz obiekt nie będący bryłą, powinien on być oparty na Part::FeaturePython, w przeciwnym razie następny obiekt w drzewie będzie próbował połączyć się z nim jako bryłą i to się nie uda.

Oto prosty przykład tworzenia elementu pierwotnego rury, podobnego do elementu pierwotnego rury w środowisku pracy Część, z tą różnicą, że ten element będzie obiektem bryłowym środowiska pracy Projekt Części. W tym celu utworzymy dwa osobne pliki: pdtube.FCMacro i pdtube.py. Plik .FCMacro będzie wykonywany przez użytkownika w celu utworzenia obiektu. Plik .py będzie zawierał definicje klas zaimportowane przez plik .FCMacro. Powodem takiego postępowania jest zachowanie parametrycznego charakteru obiektu po ponownym uruchomieniu programu FreeCAD i otwarciu dokumentu zawierającego jedną z naszych rur.

Po pierwsze, plik definicji klasy:

# -*- 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

A teraz plik makrodefinicji do utworzenia obiektu:

# -*- 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

Dostępne typy obiektów

Listę wszystkich typów obiektów, które można utworzyć za pomocą FreeCAD.ActiveDocument.addObject() można uzyskać za pomocą FreeCAD.ActiveDocument.supportedTypes(). Tylko typy obiektów o nazwie kończącej się na Python mogą być używane dla obiektów skryptowych. Są one wymienione tutaj (dla FreeCAD v0.21):

  • App::DocumentObjectGroupPython
  • App::FeaturePython
  • App::GeometryPython
  • App::LinkElementPython
  • App::LinkGroupPython
  • App::LinkPython
  • App::MaterialObjectPython
  • App::PlacementPython
  • Part::CustomFeaturePython
  • Part::FeaturePython
  • Part::Part2DObjectPython
  • Path::FeatureAreaPython
  • Path::FeatureAreaViewPython
  • Path::FeatureCompoundPython
  • Path::FeaturePython
  • Path::FeatureShapePython
  • Sketcher::SketchObjectPython
  • TechDraw::DrawComplexSectionPython
  • TechDraw::DrawLeaderLinePython
  • TechDraw::DrawPagePython
  • TechDraw::DrawRichAnnoPython
  • TechDraw::DrawTemplatePython
  • TechDraw::DrawTilePython
  • TechDraw::DrawTileWeldPython
  • TechDraw::DrawViewPartPython
  • TechDraw::DrawViewPython
  • TechDraw::DrawViewSectionPython
  • TechDraw::DrawViewSymbolPython
  • TechDraw::DrawWeldSymbolPython

Dostępne metody

Zobacz stronę Metody FeaturePython, aby zapoznać się z pełnym opisem.

Dostępne własności

Właściwości są prawdziwymi kamieniami węgielnymi obiektów FeaturePython. Dzięki nim użytkownik będzie mógł wchodzić w interakcje z obiektem i modyfikować go. Po utworzeniu nowego obiektu FeaturePython w swoim dokumencie (obj=FreeCAD.ActiveDocument.addObject("App::FeaturePython", "Box") ) możesz uzyskać listę dostępnych właściwości, wydając polecenie:

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

Zobacz Właściwości niestandardowe funkcji Python, aby uzyskać więcej informacji.

Podczas dodawania właściwości do obiektów niestandardowych należy zwrócić uwagę na następujące kwestie:

  • Nie używaj znaków < lub > w opisach właściwości (spowodowałoby to przerwanie fragmentów xml w pliku .FCStd)
  • Właściwości są przechowywane w pliku .FCStd w porządku alfabetycznym. Jeśli we właściwościach znajduje się kształt, każda właściwość, której nazwa w porządku alfabetycznym znajduje się za "Kształtem", zostanie wczytana PRZED kształtem, co może powodować dziwne zachowania.

Typy właściwości są zdefiniowane w kodzie źródłowym nagłówka C++ dla PropertyStandard.

Typy właściwości

Domyślnie właściwości mogą być aktualizowane. Możliwe jest nadanie właściwościom statusu tylko do odczytu, na przykład w sytuacji, gdy chcemy pokazać wynik działania metody. Możliwe jest także ukrycie właściwości. Typ właściwości można ustawić za pomocą:

obj.setEditorMode("MyPropertyName", mode)

gdzie mode jest krótką liczbą całkowitą, której można nadać następujące wartości:

 0 -- tryb domyślny, odczyt i zapis
 1 -- tylko do odczytu
 2 -- ukryty

Tryb można również ustawić za pomocą listy ciągów znaków, np. obj.setEditorMode("Placement", ["ReadOnly", "Hidden"]).

Tryby edytora nie są ustawiane przy ponownym wczytywaniu pliku FreeCAD. Można to zrobić za pomocą funkcji loads. Zobacz http://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=10#p108072. Dzięki użyciu setEditorMode właściwości są tylko do odczytu w Edytorze właściwości. Nadal można je zmieniać z poziomu Pythona. Aby naprawdę uczynić je tylko do odczytu, ustawienie musi być przekazane bezpośrednio wewnątrz funkcji addProperty. Przykład można znaleźć na stronie http://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=20#p109709.

Korzystając z bezpośredniego ustawienia w funkcji addProperty, masz też więcej możliwości. W szczególności interesującą możliwością jest oznaczenie właściwości jako właściwości wyjściowej. W ten sposób FreeCAD nie będzie oznaczał właściwości jako dotkniętej podczas jej zmiany (nie ma więc potrzeby ponownego obliczania).

Przykład właściwości wyjściowej (zob. też https://forum.freecadweb.org/viewtopic.php?t=24928):

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

Typy właściwości, które można ustawić w ostatnim parametrze funkcji addProperty, są następujące:

  0 -- Prop_None, brak specjalnego typu właściwości
  1 -- Prop_ReadOnly, właściwość jest tylko do odczytu w edytorze
  2 -- Prop_Transient, właściwość nie zostanie zapisana do pliku
  4 -- Prop_Hidden, właściwość nie będzie widoczna w edytorze
  8 -- Prop_Output, Zmodyfikowana właściwość nie dotyka swojego kontenera nadrzędnego
  16 -- Prop_NoRecompute, Zmodyfikowana właściwość nie dotyka swojego kontenera do rekompilacji
  32 -- Prop_NoPersist, właściwość nie zostanie zapisana do pliku

Typy właściwości są zdefiniowane w kodzie źródłowym nagłówka C++ dla PropertyContainer.

Dostępne rozszerzenia

Listę dostępnych rozszerzeń można uzyskać za pomocą grep -RI EXTENSION_PROPERTY_SOURCE_TEMPLATE w repozytorium kodu źródłowego i jest podana tutaj (dla FreeCAD v0.21).

W przypadku obiektów:

  • App::GeoFeatureGroupExtensionPython
  • App::GroupExtensionPython
  • App::LinkBaseExtensionPython
  • App::LinkExtensionPython
  • App::OriginGroupExtensionPython
  • Part::AttachExtensionPython
  • TechDraw::CosmeticExtensionPython

W przypadku obiektów widoku:

  • Gui::ViewProviderExtensionPython
  • Gui::ViewProviderGeoFeatureGroupExtensionPython
  • Gui::ViewProviderGroupExtensionPython
  • Gui::ViewProviderOriginGroupExtensionPython
  • PartGui::ViewProviderAttachExtensionPython
  • PartGui::ViewProviderSplineExtensionPython

Istnieją inne rozszerzenia, ale nie działają one w obecnej formie:

  • App::ExtensionPython
  • TechDrawGui::ViewProviderCosmeticExtensionPython
  • TechDrawGui::ViewProviderDrawingViewExtensionPython
  • TechDrawGui::ViewProviderPageExtensionPython
  • TechDrawGui::ViewProviderTemplateExtensionPython

Informacje dodatkowe

Dodatkowe strony:

Ciekawe wątki na forum dotyczące obiektów tworzonych skryptami:

Oprócz przykładów przedstawionych tutaj więcej przykładów można znaleźć w kodzie źródłowym programu FreeCAD src/Mod/TemplatePyMod/FeaturePython.py.