Difference between revisions of "FeaturePython Objects/it"

From FreeCAD Documentation
Jump to navigation Jump to search
(Created page with "Category:Python Code/it")
(Updating to match new version of source page)
 
(3 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 
<languages/>
 
<languages/>
  
 +
<div class="mw-translate-fuzzy">
 
{{docnav/it|[[PySide/it|PySide]]|[[Creating a FeaturePython Box, Part II/it|Creare un Box FeaturePython, Parte II]]}}
 
{{docnav/it|[[PySide/it|PySide]]|[[Creating a FeaturePython Box, Part II/it|Creare un Box FeaturePython, Parte II]]}}
 +
</div>
  
 
== Introduction ==
 
== Introduction ==
  
FeaturePython objects (also often referred to as 'Scripted Objects') provide users the ability to extend FreeCAD's with objects that integrate seamlessly into the FreeCAD framework.
+
{{TOCright}}
 +
 
 +
FeaturePython objects (also often referred to as [[Scripted objects]]) provide users the ability to extend FreeCAD with objects that integrate seamlessly into the FreeCAD framework.
  
 
This encourages:
 
This encourages:
 
*'''Rapid prototyping''' of new objects and tools with custom Python classes.
 
*'''Rapid prototyping''' of new objects and tools with custom Python classes.
*'''Serialization''' through 'App::Property' objects, ''without embedding any script'' in the FreeCAD document file.
+
*'''Serialization''' through {{incode|App::Property}} objects, ''without embedding any script'' in the FreeCAD document file.
 
*'''Creative freedom''' to adapt FreeCAD for any task!
 
*'''Creative freedom''' to adapt FreeCAD for any task!
  
Line 23: Line 27:
  
 
Thus, a FeaturePython object ultimately exists entirely apart from it's script. The inconvenience posed by not packing the script with the object in the document file is far less than the risk posed by running a file embedded with an unknown script.  However, the script module path is stored in the document file.  Therefore, a user need only install the custom python class code as an importable module following the same directory structure to regain the lost functionality.
 
Thus, a FeaturePython object ultimately exists entirely apart from it's script. The inconvenience posed by not packing the script with the object in the document file is far less than the risk posed by running a file embedded with an unknown script.  However, the script module path is stored in the document file.  Therefore, a user need only install the custom python class code as an importable module following the same directory structure to regain the lost functionality.
 
  
 
== Setting up your development environment ==
 
== Setting up your development environment ==
Line 29: Line 32:
 
To begin, FeaturePython Object classes need to act as importable modules in FreeCAD.  That means you need to place them in a path that exists in your Python environment (or add it specifically).  For the purposes of this tutorial, we're going to use the FreeCAD user Macro folder, though if you have another idea in mind, feel free to use that instead!
 
To begin, FeaturePython Object classes need to act as importable modules in FreeCAD.  That means you need to place them in a path that exists in your Python environment (or add it specifically).  For the purposes of this tutorial, we're going to use the FreeCAD user Macro folder, though if you have another idea in mind, feel free to use that instead!
  
If you don't know where the FreeCAD Macro folder is type 'FreeCAD.getUserMacroDir(True)' in FreeCAD's Python console. The place is configurable but, by default, to go there:
+
If you don't know where the FreeCAD Macro folder is type {{incode|FreeCAD.getUserMacroDir(True)}} in FreeCAD's [[Python console]]. The place is configurable but, by default, to go there:
*Windows: Type '%APPDATA%/FreeCAD/Macro' in the filepath bar at the top of Explorer
+
*Windows: Type {{FileName|%APPDATA%/FreeCAD/Macro}} in the filepath bar at the top of Explorer
*Linux: Navigate to /home/USERNAME/.FreeCAD/Macro
+
*Linux: Navigate to {{FileName|/home/USERNAME/.FreeCAD/Macro}}
*Mac: Navigate to /Users/USERNAME/Library/Preferences/FreeCAD/Macro
+
*Mac: Navigate to {{FileName|/Users/USERNAME/Library/Preferences/FreeCAD/Macro}}
  
 
Now we need to create some files.
 
Now we need to create some files.
*In the Macro folder create a new folder called '''fpo'''.  
+
*In the Macro folder create a new folder called {{FileName|fpo}}.  
*In the fpo folder create an empty file:  '''__init__.py'''.
+
*In the fpo folder create an empty file:  {{FileName|__init__.py}}.
*In the fpo folder, create a  new folder called '''box'''.
+
*In the fpo folder, create a  new folder called {{FileName|box}}.
*In the box folder create two files:  '''__init__.py''' and '''box.py''' (leave both empty for now)
+
*In the box folder create two files:  {{FileName|__init__.py}} and {{FileName|box.py}} (leave both empty for now)
 +
 
 +
=== Notes ===
  
Notes:
 
 
* The '''fpo''' folder provides a nice spot to play with new FeaturePython objects and the '''box''' folder is the module we will be working in.
 
* The '''fpo''' folder provides a nice spot to play with new FeaturePython objects and the '''box''' folder is the module we will be working in.
 
* '''__init__.py''' tells Python that in the folder is an importable module, and '''box.py''' will be the class file for our new FeaturePython Object.
 
* '''__init__.py''' tells Python that in the folder is an importable module, and '''box.py''' will be the class file for our new FeaturePython Object.
Line 57: Line 61:
  
 
*Start FreeCAD (if it isn't already open)
 
*Start FreeCAD (if it isn't already open)
*Enable Python Console and Report Views ('''View -> Panels -> Report view''' and '''Python console''') [[FreeCAD Scripting Basics|(learn more about it here)]]
+
*Enable [[Report view]] ({{MenuCommand|View Panels Report view}})
*In your favorite code editor, navigate to the '''/Macro/fpo/box''' folder and open '''box.py'''
+
*Enable [[Python console]] ({{MenuCommand|View → Panels → Python console}}) see [[FreeCAD Scripting Basics]]
 +
*In your favorite code editor, navigate to the '''./Macro/fpo/box''' folder and open '''box.py'''
  
 
It's time to write some code!
 
It's time to write some code!
Line 68: Line 73:
  
 
Let's get started by writing our class and it's constructor:
 
Let's get started by writing our class and it's constructor:
 
+
{{Code|code=
 
   class box():
 
   class box():
 
      
 
      
Line 77: Line 82:
 
           Arguments
 
           Arguments
 
           ---------
 
           ---------
           - obj: a variable created with FreeCAD.Document.addObject('App::FeaturePython', '{name}').
+
           - obj: an existing document object or an object created with FreeCAD.Document.addObject('App::FeaturePython', '{name}').
 
           """
 
           """
 +
 
           self.Type = 'box'
 
           self.Type = 'box'
 
      
 
      
 
           obj.Proxy = self
 
           obj.Proxy = self
 +
}}
  
  
'''The <code>__init__()</code> method breakdown'''
+
'''The {{incode|__init__()}} method breakdown'''
 
{|class="wikitable" cellpadding="5" style="margin-left: 15px; margin-right: 5px"
 
{|class="wikitable" cellpadding="5" style="margin-left: 15px; margin-right: 5px"
|style="width:30%" | <code>def __init__(self, obj):</code> || Parameters refer to the Python class itself and the FeaturePython object that it is attached to.
+
|style="width:30%" | {{incode|def __init__(self, obj):}} || Parameters refer to the Python class itself and the FeaturePython object that it is attached to.
 
|-
 
|-
 
|<code>    self.Type = 'box'</code> || String definition of the custom python type
 
|<code>    self.Type = 'box'</code> || String definition of the custom python type
Line 94: Line 101:
  
  
In the '''box.py''' file at the top, add the following code:
+
In the {{FileName|box.py}} file at the top, add the following code:
 
+
{{Code|code=
 
   import FreeCAD as App
 
   import FreeCAD as App
 
   
 
   
Line 108: Line 115:
 
   
 
   
 
       return fpo
 
       return fpo
 +
}}
  
  
'''The <code>create()</code> method breakdown'''
+
'''The {{incode|create()}} method breakdown'''
 
{|class="wikitable" cellpadding="5" style="float:right; margin-left: 15px; margin-right: 5px"
 
{|class="wikitable" cellpadding="5" style="float:right; margin-left: 15px; margin-right: 5px"
|style="width:30%" | <code>import FreeCAD as App</code> || Standard import for most python scripts.  The '''App''' alias is not required.
+
|style="width:30%" | {{incode|import FreeCAD as App}} || Standard import for most python scripts.  The '''App''' alias is not required.
 
|-
 
|-
 
|<code>obj = ... addObject(...)</code> || Creates a new FreeCAD FeaturePython object with the name passed to the method. If there is no name clash, this will be the label and the name of the created object, i.e. what is visible in the model tree. Otherwise, a unique name and label will be created based on 'obj_name'.
 
|<code>obj = ... addObject(...)</code> || Creates a new FreeCAD FeaturePython object with the name passed to the method. If there is no name clash, this will be the label and the name of the created object, i.e. what is visible in the model tree. Otherwise, a unique name and label will be created based on 'obj_name'.
 
|-
 
|-
|<code>fpo = box(obj)</code> || Create our custom class instance and link it to the FeaturePython object.
+
|<code>fpo = box(obj)</code || Create our custom class instance and link it to the FeaturePython object.
 
|}
 
|}
  
  
The <code>create()</code> method is not required, but it provides a nice way to encapsulate the object creation code.
+
The {{incode|create()}} method is not required, but it provides a nice way to encapsulate the object creation code.
  
  
Line 127: Line 135:
 
=== Testing the Code ===
 
=== Testing the Code ===
  
Now we can try our new object.  Save your code and return to FreeCAD, make sure you've '''opened a new document'''.  You can do this by pressing '''CTRL+n''' or selecting '''File -> New'''
+
Now we can try our new object.  Save your code and return to FreeCAD, make sure you've '''opened a new document'''.  You can do this by pressing {{KEY|CTRL}}+{{KEY|n}} or selecting {{MenuCommand|File New}}
  
 
In the Python Console, type the following:
 
In the Python Console, type the following:
  
>>> from fpo.box import box
+
{{Code|code=
 +
>>> from fpo.box import box
 +
}}
  
<br>Now, we need to create our object:
+
Now, we need to create our object:
  
>>> box.create('my_box')
+
{{Code|code=
 +
>>> box.create('my_box')
 +
}}
  
 
[[File:Fpo_treeview.png | right | 192px]]
 
[[File:Fpo_treeview.png | right | 192px]]
 
You should see a new object appear in the tree view at the top left labelled '''my_box'''.<br><br>Note that the icon is gray.  FreeCAD is simply telling us that the object is not able to display anything in the 3D view... yet.  Click on the object and note what appears in the property panel under it.<br>There's not very much - just the name of the object.  We'll need to add some properties in a bit.
 
You should see a new object appear in the tree view at the top left labelled '''my_box'''.<br><br>Note that the icon is gray.  FreeCAD is simply telling us that the object is not able to display anything in the 3D view... yet.  Click on the object and note what appears in the property panel under it.<br>There's not very much - just the name of the object.  We'll need to add some properties in a bit.
<br clear="all">
+
{{Clear}}
 
Let's also make referencing our new object a little more convenient:
 
Let's also make referencing our new object a little more convenient:
  
>>> mybox = App.ActiveDocument.my_box
+
{{Code|code=
 +
>>> mybox = App.ActiveDocument.my_box
 +
}}
  
 
And then we should take a look at our object's attributes:
 
And then we should take a look at our object's attributes:
 +
{{Code|code=
 +
>>> dir(mybox)
 +
['Content', 'Document', 'ExpressionEngine', 'InList', 'InListRecursive', 'Label', 'MemSize', 'Module', 'Name', 'OutList', 'OutListRecursive', 'PropertiesList', 'Proxy', 'State', 'TypeId', 'ViewObject', '__class__',
 +
...
 +
'setEditorMode', 'setExpression', 'supportedProperties', 'touch']
 +
}}
  
>>> dir(mybox)
 
['Content', 'Document', 'ExpressionEngine', 'InList', 'InListRecursive', 'Label', 'MemSize', 'Module', 'Name', 'OutList', 'OutListRecursive', 'PropertiesList', 'Proxy', 'State', 'TypeId', 'ViewObject', '__class__',
 
  ...
 
  'setEditorMode', 'setExpression', 'supportedProperties', 'touch']
 
  
 +
There's a lot of attributes there because we're accessing the native FreeCAD FeaturePyton object that we created in the first line of our {{incode|create()}} method.
 +
The {{incode|Proxy}} property we added in our {{incode|__init__()}} method is there, too.
  
There's a lot of attributes there because we're accessing the native FreeCAD FeaturePyton object that we created in the first line of our <code>create()</code> method.
+
Let's inspect that by calling the {{incode|dir()}}on the Proxy object:
The <code>Proxy</code> property we added in our <code>__init__()</code> method is there, too.
+
{{Code|code=
 
 
Let's inspect that by calling the <code>dir()</code>on the Proxy object:
 
 
 
 
  >>> dir(mybox.Proxy)
 
  >>> dir(mybox.Proxy)
 
  ['Object', 'Type', '__class__', '__delattr__', '__dict__', '__dir__',  
 
  ['Object', 'Type', '__class__', '__delattr__', '__dict__', '__dir__',  
 
   ...
 
   ...
 
   '__str__', '__subclasshook__', '__weakref__']
 
   '__str__', '__subclasshook__', '__weakref__']
 +
}}
  
  
Once we inspect the Proxy property, we can see our <code>Object</code> and <code>Type</code> properties.  This means we're accessing the custom Python object defined in  '''box.py'''.
+
Once we inspect the Proxy property, we can see our {{incode|Object}} and {{incode|Type}} properties.  This means we're accessing the custom Python object defined in  {{FileName|box.py}}.
  
Call the <code>Type</code> property and look at the result:
+
Call the {{incode|Type}} property and look at the result:
>>> mybox.Proxy.Type
+
{{Code|code=
'box'
+
>>> mybox.Proxy.Type
 +
'box'
 +
}}
  
 
Sure enough, it returns the value we assigned, so we know we're accessing the custom class itself through the FeaturePython object.
 
Sure enough, it returns the value we assigned, so we know we're accessing the custom class itself through the FeaturePython object.
  
Likewise, we can access the FreeCAD object (not our Python object) by using the<code>Object</code>method:
+
Likewise, we can access the FreeCAD object (not our Python object) by using the{{incode|Object}}method:
>>> mybox.Proxy.Object
+
{{Code|code=
 +
>>> mybox.Proxy.Object
 +
}}
  
 
That was fun!  But now let's see if we can make our class a little more interesting... and maybe more useful.
 
That was fun!  But now let's see if we can make our class a little more interesting... and maybe more useful.
Line 182: Line 202:
  
 
Properties are the lifeblood of a FeaturePython class.
 
Properties are the lifeblood of a FeaturePython class.
<br><br>Fortunately, FreeCAD supports [[FeaturePython_Custom_Properties|a number of property types]] for FeaturePython classes.  These properties are attached directly to the FeaturePython object itself and fully serialized when the file is saved.  That means, unless you want to serialize the data yourself, you'll need to find some way to wrangle it into a supported property type.
 
<br><br>Adding properties is done quite simply using the <code>add_property()</code> method.  The syntax for the method is:
 
  
  add_property(type, name, section, description)
+
Fortunately, FreeCAD supports [[FeaturePython_Custom_Properties|a number of property types]] for FeaturePython classes.  These properties are attached directly to the FeaturePython object itself and fully serialized when the file is saved.  That means, unless you want to serialize the data yourself, you'll need to find some way to wrangle it into a supported property type.
 +
 
 +
Adding properties is done quite simply using the {{incode|add_property()}} method. The syntax for the method is:
 +
 
 +
{{Code|code=
 +
{{incode|add_property(type, name, section, description)}}
 +
}}
  
 
{|class="wikitable" cellpadding="5" style="float:right; margin-left: 15px; margin-right: 5px; text-align:left"
 
{|class="wikitable" cellpadding="5" style="float:right; margin-left: 15px; margin-right: 5px; text-align:left"
 
!Tip
 
!Tip
 
|-
 
|-
||You can view the list of supported properties for an object by typing:<br><code>>>> mybox.supportedProperties()</code>
+
||You can view the list of supported properties for an object by typing:<br>{{incode|>>> mybox.supportedProperties()}}
 
|}
 
|}
 
<br>Let's try adding a property to our box class.
 
<br>Let's try adding a property to our box class.
<br><br>Switch to your code editor and move to the <code>__init__()</code> method.
+
<br><br>Switch to your code editor and move to the {{incode|__init__()}} method.
  
<br clear="all">Then, at the end of the method, add:
+
{{clear}}
  
obj.addProperty('App::PropertyString', 'Description', 'Base', 'Box description').Description = ""
+
Then, at the end of the method, add:
Note how we're using the reference to the (serializable) FeaturePython object, <code>obj</code> and not the (non-serializable) Python class instanace, <code>self</code>.
+
{{Code|code=
 +
obj.addProperty('App::PropertyString', 'Description', 'Base', 'Box description').Description = ""
 +
}}
 +
Note how we're using the reference to the (serializable) FeaturePython object, {{incode|obj}} and not the (non-serializable) Python class instance, {{incode|self}}.
 
<br>Anyway, once you're done, save the changes and switch back to FreeCAD.
 
<br>Anyway, once you're done, save the changes and switch back to FreeCAD.
 
<br><br>Before we can observe the changes we made to our code, we need to reload the module.  This can be accomplished by restarting FreeCAD, but restarting FreeCAD everytime we make a change to the python class code can get a bit inconvenient. To make it easier, try the following in the Python console:
 
<br><br>Before we can observe the changes we made to our code, we need to reload the module.  This can be accomplished by restarting FreeCAD, but restarting FreeCAD everytime we make a change to the python class code can get a bit inconvenient. To make it easier, try the following in the Python console:
 
+
{{Code|code=
>>> from importlib import reload
+
>>> from importlib import reload
>>> reload(box)
+
>>> reload(box)
This will reload the box module, incorporating changes you made to the '''box.py''' file, just as if you'd restarted FreeCAD.
+
}}
 +
This will reload the box module, incorporating changes you made to the {{FileName|box.py}} file, just as if you'd restarted FreeCAD.
 
<br><br>With the module reloaded, now let's see what we get when we create an object:
 
<br><br>With the module reloaded, now let's see what we get when we create an object:
 +
{{Code|code=
 +
>>> box.create('box_property_test')}}
 +
[[File:fpo_properties.png | right | 320px]]You should see the new box object appear in the [[Tree view]] on the left.
  
>>> box.create('box_property_test')
+
*Select it and look at the [[Property editor]].  There, you should see the 'Description' property.
[[File:fpo_properties.png | right | 320px]]You should see the new box object appear in the tree view at left.<br><br>
+
*Hover over the property name at left and see the tooltip appear with the description text you provided.
*Select it and look at the Property Panel.  There, you should see the 'Description' property.<br>
 
*Hover over the property name at left and see the tooltip appear with the description text you provided.<br>
 
 
*Select the field and type whatever you like.  You'll notice that Python update commands are executed and displayed in the console as you type letters and the property changes.
 
*Select the field and type whatever you like.  You'll notice that Python update commands are executed and displayed in the console as you type letters and the property changes.
<br><br>But before we leave the topic of properties for the moment, let's go back and add some properties that would make a custom box object *really* useful:  namely, ''length'', ''width'', and ''height''.
+
 
<br><br>Return to your source code and add the following properties to <code>__init__()</code>:
+
But before we leave the topic of properties for the moment, let's go back and add some properties that would make a custom box object *really* useful:  namely, ''length'', ''width'', and ''height''. Return to your source code and add the following properties:
 +
{{Code|code=
 +
__init__():
  
 
  obj.addProperty('App::PropertyLength', 'Length', 'Dimensions', 'Box length').Length = 10.0
 
  obj.addProperty('App::PropertyLength', 'Length', 'Dimensions', 'Box length').Length = 10.0
 
  obj.addProperty('App::PropertyLength', 'Width', 'Dimensions', 'Box width').Width = '10 mm'
 
  obj.addProperty('App::PropertyLength', 'Width', 'Dimensions', 'Box width').Width = '10 mm'
 
  obj.addProperty('App::PropertyLength', 'Height', 'Dimensions', 'Box height').Height = '1 cm'
 
  obj.addProperty('App::PropertyLength', 'Height', 'Dimensions', 'Box height').Height = '1 cm'
 +
}}
 +
[[File:object_recompute_icon.png | left | 64px]]One last thing:  Did you notice how the blue checkmark appears next to the FeaturePython object in the [[tree view]] ?
 +
That's because when an object is created or changed, it's "touched" and needs to be [[Std_Refresh|recomputed]].  Clicking the [[Image:Std_Refresh.svg|24px|link=Std_Refresh]] "recycle" arrows (the two arrows forming a circle) will accomplish this.
 +
<br><br>But, we can accomplish that automatically by adding the following line to the end of the {{incode|create()}} method:
  
[[File:object_recompute_icon.png | left | 64px]]One last thing:  Did you notice how the blue checkmark appears next to the FeaturePython object in the treeview at left?
+
{{Code|code=
That's because when an object is created or changed, it's "touched" and needs to be recomputed.  Clicking the "recycle" arrows (the two arrows forming a circle) will accomplish this.
+
App.ActiveDocument.recompute()
<br><br>But, we can accomplish that automatically by adding the following line to the end of the <code>create()</code> method:
+
}}
 
 
App.ActiveDocument.recompute()
 
 
{|class="wikitable" cellpadding="5" style="margin-left: 15px; margin-right: 15px; text-align:left"
 
{|class="wikitable" cellpadding="5" style="margin-left: 15px; margin-right: 15px; text-align:left"
 
!Note
 
!Note
Line 229: Line 262:
 
|Be careful where you recompute a FeaturePython object!  Generally, you should not try to recompute an object from within itself.  Rather, document recomputing should be handled by a method external to the object, if possible.
 
|Be careful where you recompute a FeaturePython object!  Generally, you should not try to recompute an object from within itself.  Rather, document recomputing should be handled by a method external to the object, if possible.
 
|}
 
|}
<br>Now, test your changes as follows:<br>
+
<br>Now, test your changes as follows:
*Save your changes and return to FreeCAD.<br>
+
*Save your changes and return to FreeCAD.
*Delete any existing objects and reload your module.<br>
+
*Delete any existing objects and reload your module.
*Finally, create another box object from the command line by calling <code>>>> box.create('myBox')</code>.
+
*Finally, create another box object from the command line by calling {{incode|>>> box.create('myBox')}}
 
[[File:fpo_box_properties.png | right | 320px]]
 
[[File:fpo_box_properties.png | right | 320px]]
<br>Once the box is created (and you've checked to make sure it's been recomputed!), select the object and look at your properties.   
+
Once the box is created (and you've checked to make sure it's been recomputed!), select the object and look at your properties.  You should note two things:
<br>You should note two things:<br>
+
 
 
*Three new properties (''length'', ''width'', and ''height'')
 
*Three new properties (''length'', ''width'', and ''height'')
 
*A new property group, ''Dimensions''.
 
*A new property group, ''Dimensions''.
<br>Note also how the properties have dimensions.  Specifically, they take on the linear dimension of the units set in the user preferences (see '''Edit''' -> '''Preference...''' -> '''Units tab''').
+
 
<br clear="all"><br>In fact, if you were paying attention when you were entering the code, you will have noticed that three separate values were entered for each dimension.  The length was a floating-point value (10.0), the width was a string, specifying millimeters ('10 mm') and the height was a string specifying centimeters ('1 cm').  Yet, the property rendered all three values the same way:  10 mm.  Specifically, a floating-point value is assumed to be in the current document units, and the string values are parsed according to the units specified, then converted to document units.
+
Note also how the properties have dimensions.  Specifically, they take on the linear dimension of the units set in the user preferences ({{MenuCommand|Edit Preference... Units tab}}).{{clear}}<br>In fact, if you were paying attention when you were entering the code, you will have noticed that three separate values were entered for each dimension.  The length was a floating-point value (10.0), the width was a string, specifying millimeters ('10 mm') and the height was a string specifying centimeters ('1 cm').  Yet, the property rendered all three values the same way:  10 mm.  Specifically, a floating-point value is assumed to be in the current document units, and the string values are parsed according to the units specified, then converted to document units.
<br><br>The nice thing about the <code>App::PropertyLength</code> type is that it's a 'unit' type - values are understood as having specific units. Therefore, whenever you create a property that uses linear dimensions, use <code>App::PropertyLength</code> as the property type.
+
 
 +
The nice thing about the {{incode|App::PropertyLength}} type is that it's a 'unit' type - values are understood as having specific units. Therefore, whenever you create a property that uses linear dimensions, use {{incode|App::PropertyLength}} as the property type.
  
  
Line 247: Line 281:
 
=== Event Trapping ===
 
=== Event Trapping ===
  
The last element required for a basic FeaturePython object is event trapping.  Specifically, we need to trap the <code>execute()</code> event, which is called when the object is recomputed. There's several other document-level events that can be trapped in our object as well, both in the FeaturePython object itself and in the ViewProvider, which we'll cover in another section.
+
The last element required for a basic FeaturePython object is event trapping.  Specifically, we need to trap the {{incode|execute()}} event, which is called when the object is recomputed. There's several other document-level events that can be trapped in our object as well, both in the FeaturePython object itself and in the [[ViewProvider]], which we'll cover in another section.
<br>
 
Add the following after the <code>__init__()</code> function:
 
  
def execute(self, obj):
+
Add the following after the {{incode|__init__()}} function:
 +
{{Code|code=
 +
def execute(self, obj):
 
     """
 
     """
 
     Called on document recompute
 
     Called on document recompute
Line 257: Line 291:
 
   
 
   
 
     print('Recomputing {0:s} ({1:s})'.format(obj.Name, self.Type))
 
     print('Recomputing {0:s} ({1:s})'.format(obj.Name, self.Type))
 
+
}}
  
 
Test the code  as follows:
 
Test the code  as follows:
*Save changes and reload the box module in the FreeCAD python console.
+
*Save changes and reload the box module in the FreeCAD [[Python console]].
*Delete any objects in the Treeview
+
*Delete any objects in the [[Tree view]]
 
*Re-create the box object.
 
*Re-create the box object.
  
You should see the printed output in the Python Console, thanks to the <code>recompute()</code> call we added to the <code>create()</code> method.
+
You should see the printed output in the Python Console, thanks to the {{incode|recompute()}} call we added to the {{incode|create()}} method.
  
 
Of course, the <code>execute()</code> method doesn't do anything here (except tell us that it was called), but it is the key to the magic of FeaturePython objects.
 
Of course, the <code>execute()</code> method doesn't do anything here (except tell us that it was called), but it is the key to the magic of FeaturePython objects.
  
<br>'''So that's it!'''
+
'''So that's it!'''
<br>You now know how to build a basic, functional FeaturePython object!
+
 
 +
You now know how to build a basic, functional FeaturePython object!
  
  
Line 275: Line 310:
  
 
=== The Completed Code ===
 
=== The Completed Code ===
 +
{{Code|code=
 
  import FreeCAD as App
 
  import FreeCAD as App
 
   
 
   
Line 286: Line 322:
 
     fpo = box(obj)
 
     fpo = box(obj)
 
   
 
   
     return fpo
+
     return obj
 
   
 
   
 
  class box():
 
  class box():
Line 310: Line 346:
 
   
 
   
 
         print('Recomputing {0:s} {1:s}'.format(obj.Name, self.Type))
 
         print('Recomputing {0:s} {1:s}'.format(obj.Name, self.Type))
{{docnav|PySide|Creating a FeaturePython Box, Part II}}
+
}}
 
+
<div class="mw-translate-fuzzy">
{{Userdocnavi/it}}
+
{{docnav/it|[[PySide/it|PySide]]|[[Creating a FeaturePython Box, Part II/it|Creare un Box FeaturePython, Parte II]]}}
 
+
</div>
[[Category:Poweruser Documentation/it]]
 
  
[[Category:Python Code/it]]
+
{{Userdocnavi{{#translation:}}}}
 +
[[Category:Poweruser Documentation{{#translation:}}]]
 +
[[Category:Python Code{{#translation:}}]]

Latest revision as of 19:52, 15 May 2020

Other languages:
English • ‎français • ‎italiano • ‎русский
Arrow-left.svg PySide Pagina precedente:

Introduction

FeaturePython objects (also often referred to as Scripted objects) provide users the ability to extend FreeCAD with objects that integrate seamlessly into the FreeCAD framework.

This encourages:

  • Rapid prototyping of new objects and tools with custom Python classes.
  • Serialization through App::Property objects, without embedding any script in the FreeCAD document file.
  • Creative freedom to adapt FreeCAD for any task!

This wiki will provide you with a complete understanding of how to use FeaturePython objects and custom Python classes in FreeCAD. We're going to construct a complete, working example of a FeaturePython custom class, identifying all of the major components and gaining an intimate understanding of how everything works as we go.

How Does It Work?

FreeCAD comes with a number of default object types for managing different kinds of geometry. Some of them have 'FeaturePython' alternatives that allow for user customization with a custom python class.

The custom python class simply takes a reference to one of these objects and modifies it in any number of ways. For example, the python class may add properties directly to the object, modifying other properties when it's recomputed, or linking it to other objects. In addition the python class implements certain methods to enable it to respond to document events, making it possible to trap object property changes and document recomputes.

It's important to remember, however, that for as much as one can accomplish with custom classes and FeaturePython objects, when it comes time to save the document, only the FeaturePython object itself is serialized. The custom class and it's state are not retained between document reloading. Doing so would require embedding script in the FreeCAD document file, which poses a significant security risk, much like the risks posed by embedding VBA macros in Microsoft Office documents.

Thus, a FeaturePython object ultimately exists entirely apart from it's script. The inconvenience posed by not packing the script with the object in the document file is far less than the risk posed by running a file embedded with an unknown script. However, the script module path is stored in the document file. Therefore, a user need only install the custom python class code as an importable module following the same directory structure to regain the lost functionality.

Setting up your development environment

To begin, FeaturePython Object classes need to act as importable modules in FreeCAD. That means you need to place them in a path that exists in your Python environment (or add it specifically). For the purposes of this tutorial, we're going to use the FreeCAD user Macro folder, though if you have another idea in mind, feel free to use that instead!

If you don't know where the FreeCAD Macro folder is type FreeCAD.getUserMacroDir(True) in FreeCAD's Python console. The place is configurable but, by default, to go there:

  • Windows: Type %APPDATA%/FreeCAD/Macro in the filepath bar at the top of Explorer
  • Linux: Navigate to /home/USERNAME/.FreeCAD/Macro
  • Mac: Navigate to /Users/USERNAME/Library/Preferences/FreeCAD/Macro

Now we need to create some files.

  • In the Macro folder create a new folder called fpo.
  • In the fpo folder create an empty file: __init__.py.
  • In the fpo folder, create a new folder called box.
  • In the box folder create two files: __init__.py and box.py (leave both empty for now)

Notes

  • The fpo folder provides a nice spot to play with new FeaturePython objects and the box folder is the module we will be working in.
  • __init__.py tells Python that in the folder is an importable module, and box.py will be the class file for our new FeaturePython Object.

Your directory structure should look like this:

.FreeCAD
  |--> Macro
       |--> fpo
            |--> __init__.py
            |--> box
                 |--> __init__.py
                 |--> box.py

With our module paths and files created, let's make sure FreeCAD is set up properly:

  • Start FreeCAD (if it isn't already open)
  • Enable Report view (View → Panels → Report view)
  • Enable Python console (View → Panels → Python console) see FreeCAD Scripting Basics
  • In your favorite code editor, navigate to the ./Macro/fpo/box folder and open box.py

It's time to write some code!



A Very Basic FeaturePython Object

Let's get started by writing our class and it's constructor:

class box():
    
      def __init__(self, obj):
          """
          Constructor
          
          Arguments
          ---------
          - obj: an existing document object or an object created with FreeCAD.Document.addObject('App::FeaturePython', '{name}').
          """

          self.Type = 'box'
    
          obj.Proxy = self


The __init__() method breakdown

def __init__(self, obj): Parameters refer to the Python class itself and the FeaturePython object that it is attached to.
self.Type = 'box' String definition of the custom python type
obj.Proxy = self Stores a reference to the Python instance in the FeaturePython object


In the box.py file at the top, add the following code:

import FreeCAD as App
 
  def create(obj_name):
      """
      Object creation method
      """
 
      obj = App.ActiveDocument.addObject('App::FeaturePython', obj_name)
 
      fpo = box(obj)
 
      return fpo


The create() method breakdown

import FreeCAD as App Standard import for most python scripts. The App alias is not required.
obj = ... addObject(...) Creates a new FreeCAD FeaturePython object with the name passed to the method. If there is no name clash, this will be the label and the name of the created object, i.e. what is visible in the model tree. Otherwise, a unique name and label will be created based on 'obj_name'.
fpo = box(obj)</code Create our custom class instance and link it to the FeaturePython object.


The create() method is not required, but it provides a nice way to encapsulate the object creation code.



Testing the Code

Now we can try our new object. Save your code and return to FreeCAD, make sure you've opened a new document. You can do this by pressing CTRL+n or selecting File → New

In the Python Console, type the following:

>>> from fpo.box import box

Now, we need to create our object:

>>> box.create('my_box')
Fpo treeview.png

You should see a new object appear in the tree view at the top left labelled my_box.

Note that the icon is gray. FreeCAD is simply telling us that the object is not able to display anything in the 3D view... yet. Click on the object and note what appears in the property panel under it.
There's not very much - just the name of the object. We'll need to add some properties in a bit.

Let's also make referencing our new object a little more convenient:

>>> mybox = App.ActiveDocument.my_box

And then we should take a look at our object's attributes:

>>> dir(mybox)
['Content', 'Document', 'ExpressionEngine', 'InList', 'InListRecursive', 'Label', 'MemSize', 'Module', 'Name', 'OutList', 'OutListRecursive', 'PropertiesList', 'Proxy', 'State', 'TypeId', 'ViewObject', '__class__', 
 ...
'setEditorMode', 'setExpression', 'supportedProperties', 'touch']


There's a lot of attributes there because we're accessing the native FreeCAD FeaturePyton object that we created in the first line of our create() method. The Proxy property we added in our __init__() method is there, too.

Let's inspect that by calling the dir()on the Proxy object:

>>> dir(mybox.Proxy)
 ['Object', 'Type', '__class__', '__delattr__', '__dict__', '__dir__', 
  ...
  '__str__', '__subclasshook__', '__weakref__']


Once we inspect the Proxy property, we can see our Object and Type properties. This means we're accessing the custom Python object defined in box.py.

Call the Type property and look at the result:

>>> mybox.Proxy.Type
'box'

Sure enough, it returns the value we assigned, so we know we're accessing the custom class itself through the FeaturePython object.

Likewise, we can access the FreeCAD object (not our Python object) by using theObjectmethod:

>>> mybox.Proxy.Object

That was fun! But now let's see if we can make our class a little more interesting... and maybe more useful.



Adding Properties

Properties are the lifeblood of a FeaturePython class.

Fortunately, FreeCAD supports a number of property types for FeaturePython classes. These properties are attached directly to the FeaturePython object itself and fully serialized when the file is saved. That means, unless you want to serialize the data yourself, you'll need to find some way to wrangle it into a supported property type.

Adding properties is done quite simply using the add_property() method. The syntax for the method is:

<code>add_property(type, name, section, description)</code>
Tip
You can view the list of supported properties for an object by typing:
>>> mybox.supportedProperties()


Let's try adding a property to our box class.

Switch to your code editor and move to the __init__() method.


Then, at the end of the method, add:

obj.addProperty('App::PropertyString', 'Description', 'Base', 'Box description').Description = ""

Note how we're using the reference to the (serializable) FeaturePython object, obj and not the (non-serializable) Python class instance, self.
Anyway, once you're done, save the changes and switch back to FreeCAD.

Before we can observe the changes we made to our code, we need to reload the module. This can be accomplished by restarting FreeCAD, but restarting FreeCAD everytime we make a change to the python class code can get a bit inconvenient. To make it easier, try the following in the Python console:

>>> from importlib import reload
>>> reload(box)

This will reload the box module, incorporating changes you made to the box.py file, just as if you'd restarted FreeCAD.

With the module reloaded, now let's see what we get when we create an object:

>>> box.create('box_property_test')
Fpo properties.png

You should see the new box object appear in the Tree view on the left.

  • Select it and look at the Property editor. There, you should see the 'Description' property.
  • Hover over the property name at left and see the tooltip appear with the description text you provided.
  • Select the field and type whatever you like. You'll notice that Python update commands are executed and displayed in the console as you type letters and the property changes.

But before we leave the topic of properties for the moment, let's go back and add some properties that would make a custom box object *really* useful: namely, length, width, and height. Return to your source code and add the following properties:

__init__():

 obj.addProperty('App::PropertyLength', 'Length', 'Dimensions', 'Box length').Length = 10.0
 obj.addProperty('App::PropertyLength', 'Width', 'Dimensions', 'Box width').Width = '10 mm'
 obj.addProperty('App::PropertyLength', 'Height', 'Dimensions', 'Box height').Height = '1 cm'
Object recompute icon.png

One last thing: Did you notice how the blue checkmark appears next to the FeaturePython object in the tree view ?

That's because when an object is created or changed, it's "touched" and needs to be recomputed. Clicking the Std Refresh.svg "recycle" arrows (the two arrows forming a circle) will accomplish this.

But, we can accomplish that automatically by adding the following line to the end of the create() method:

App.ActiveDocument.recompute()
Note
Be careful where you recompute a FeaturePython object! Generally, you should not try to recompute an object from within itself. Rather, document recomputing should be handled by a method external to the object, if possible.


Now, test your changes as follows:

  • Save your changes and return to FreeCAD.
  • Delete any existing objects and reload your module.
  • Finally, create another box object from the command line by calling >>> box.create('myBox')
Fpo box properties.png

Once the box is created (and you've checked to make sure it's been recomputed!), select the object and look at your properties. You should note two things:

  • Three new properties (length, width, and height)
  • A new property group, Dimensions.

Note also how the properties have dimensions. Specifically, they take on the linear dimension of the units set in the user preferences (Edit → Preference... → Units tab).


In fact, if you were paying attention when you were entering the code, you will have noticed that three separate values were entered for each dimension. The length was a floating-point value (10.0), the width was a string, specifying millimeters ('10 mm') and the height was a string specifying centimeters ('1 cm'). Yet, the property rendered all three values the same way: 10 mm. Specifically, a floating-point value is assumed to be in the current document units, and the string values are parsed according to the units specified, then converted to document units.

The nice thing about the App::PropertyLength type is that it's a 'unit' type - values are understood as having specific units. Therefore, whenever you create a property that uses linear dimensions, use App::PropertyLength as the property type.



Event Trapping

The last element required for a basic FeaturePython object is event trapping. Specifically, we need to trap the execute() event, which is called when the object is recomputed. There's several other document-level events that can be trapped in our object as well, both in the FeaturePython object itself and in the ViewProvider, which we'll cover in another section.

Add the following after the __init__() function:

def execute(self, obj):
     """
     Called on document recompute
     """
 
     print('Recomputing {0:s} ({1:s})'.format(obj.Name, self.Type))

Test the code as follows:

  • Save changes and reload the box module in the FreeCAD Python console.
  • Delete any objects in the Tree view
  • Re-create the box object.

You should see the printed output in the Python Console, thanks to the recompute() call we added to the create() method.

Of course, the execute() method doesn't do anything here (except tell us that it was called), but it is the key to the magic of FeaturePython objects.

So that's it!

You now know how to build a basic, functional FeaturePython object!



The Completed Code

import FreeCAD as App
 
 def create(obj_name):
    """
    Object creation method
    """
 
    obj = App.ActiveDocument.addObject('App::FeaturePython', obj_name))
 
    fpo = box(obj)
 
    return obj
 
 class box():
 
    def __init__(self, obj):
        """
        Default Constructor
        """
 
        self.Type = 'box'
 
       obj.addProperty('App::PropertyString', 'Description', 'Base', 'Box description').Description = ""
       obj.addProperty('App::PropertyLength', 'Length', 'Dimensions', 'Box length').Length = 10.0
       obj.addProperty('App::PropertyLength', 'Width', 'Dimensions', 'Box width').Width = '10 mm'
       obj.addProperty('App::PropertyLength', 'Height', 'Dimensions', 'Box height').Height = '1 cm'
 
        obj.Proxy = self
 
    def execute(self, obj):
        """
        Called on document recompute
        """
 
        print('Recomputing {0:s} {1:s}'.format(obj.Name, self.Type))
Arrow-left.svg PySide Pagina precedente: