Create a FeaturePython object part II/it: Difference between revisions
Renatorivo (talk | contribs) (Created page with "Creare un Box FeaturePython, Parte II") |
(Updating to match new version of source page) |
||
(27 intermediate revisions by 3 users not shown) | |||
Line 1: | Line 1: | ||
<languages/> |
<languages/> |
||
{{Docnav/it |
|||
{{docnav|FeaturePython Objects|Scripting examples}} |
|||
|[[Create_a_FeaturePython_object_part_I/it|Create a FeaturePython object part I]] |
|||
| |
|||
|IconL= |
|||
|IconR= |
|||
}} |
|||
{{TOCright}} |
|||
=== App::FeaturePython vs. Part::FeaturePython === |
|||
<span id="Introduction"></span> |
|||
To this point, we've focused on the internal aspects of a Python class built around a FeaturePython object - specifically, an App::FeaturePython object.<br> |
|||
==Introduzione== |
|||
We've created the object, defined some properties, and added some document-level event callbacks that allow our object to respond to a document recompute with the <code>execute()</code> method. |
|||
On the [[Create_a_FeaturePython_object_part_I|Create a FeaturePython object part I]] page we've focused on the internal aspects of a Python class built around a FeaturePython object, specifically an {{incode|App::FeaturePython}} object. We've created the object, defined some properties, and added a document-level event callback that allows our object to respond to a document recompute with the {{incode|execute()}} method. But our object still lacks a presence in the [[3D_view|3D view]]. Let's remedy that by adding a box. |
|||
'''But we still don't have a box.''' |
|||
==Adding a box== |
|||
We can easily create one, however, by adding just two lines. |
|||
First |
First at the top of the {{FileName|box.py}} file, below the existing import, add: |
||
Then, in {{incode|execute()}}, add the following line (you can delete the {{incode|print()}} statement): |
|||
{{Code|code= |
{{Code|code= |
||
import Part |
|||
def execute(): |
|||
""" |
|||
Called on document recompute |
|||
""" |
|||
Part.show(Part.makeBox(obj.Length, obj.Width, obj.Height) |
|||
}} |
}} |
||
These commands execute python scripts that come with FreeCAD as a default. |
|||
*The <code>makeBox()</code> method generates a new box Shape. |
|||
*The enclosing call to <code>Part.show()</code> adds the Shape to the document tree and makes it visible. |
|||
Then in {{incode|execute()}} delete the {{incode|print()}} statement and add the following line in its place: |
|||
If you have FreeCAD open, reload the box module and create a new box object using {{incode|box.create()}} (delete any existing box objects, just to keep things clean). |
|||
{{Code|code= |
|||
Notice how a box immediately appears on the screen. that's because the execute method is called immediately after the box is created, because we force the document to recompute at the end of <code>box.create()</code> |
|||
Part.show(Part.makeBox(obj.Length, obj.Width, obj.Height)) |
|||
}} |
|||
[[Image:App_featurepython_box.png | right]] |
|||
These commands execute Python methods that come with FreeCAD by default: |
|||
[[File:App_featurepython_box.png]] |
|||
*The {{incode|Part.makeBox()}} method generates a box shape. |
|||
*The enclosing call to {{incode|Part.show()}} adds the shape to the document and makes it visible. |
|||
Delete any existing objects, reload the box module and create a new box object using {{incode|box.create()}}. Notice how a box immediately appears on the screen. That is because we force the document to recompute at the end of {{incode|box.create()}} which in turn triggers the {{incode|execute()}} method of our {{incode|box}} class. |
|||
'''But there's a problem.''' |
|||
At first glance the result may look fine but there are some problems. The most obvious one is that the box is represented by an entirely different object than our FeaturePython object. {{incode|Part.show()}} simply creates a separate box object and adds it to the document. Worse, if you change the dimensions of the FeaturePython object another box shape gets created, and the old one is left in place. And if you have the [[Report_view|Report view]] open, you may have noticed an error stating "Recursive calling of recompute for document Unnamed". This has to do with using the {{incode|Part.show()}} method inside a FeaturePython object. |
|||
{{clear}} |
|||
[[#top|top]] |
|||
'''Tip''' |
|||
==Fixing the code== |
|||
The problem is, we're relying on the {{incode|Part.make*()}} methods, which only generate non-parametric Part::Feature objects (simple, dumb shapes), just as you'd get if you copied a parametric object using [[Part CreateSimpleCopy|Part Simple Copy]]. |
|||
To solve these problems we have to make a number of changes. Until now we've been using a {{incode|App::FeaturePython}} object which is actually not intended to have a visual representation in the 3D view. We have to use a {{incode|Part::FeaturePython}} object instead. In {{incode|create()}} change the following line: |
|||
What we want, of course, is a '''parametric''' box object that resizes the existing box as we change it's properties. We could delete the previous Part::Feature object and regenerate it every time we change a property, but we still have two objects to manage - our custom FeaturePython object and the Part::Feature object it generates. |
|||
{{Code|code= |
|||
'''So how do we solve this problem?''' |
|||
obj = App.ActiveDocument.addObject('App::FeaturePython', obj_name) |
|||
}} |
|||
to: |
|||
First, we need to use the right type of object. |
|||
{{Code|code= |
|||
To this point we've been using {{incode|App::FeaturePython}} objects. They're great, but they're not intended for use as geometry objects. Rather, they are better used as document objects which do not require a visual representation in the 3D view. So we need to use a {{incode|Part::FeaturePython}} object ibstead. |
|||
obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name) |
|||
}} |
|||
To get rid of the separate box object we need to assigns the result of the {{incode|makeBox()}} method to the {{incode|Shape}} property of our {{incode|Part::FeaturePython}} object. Change this line in {{incode|execute()}}: |
|||
{{Code|code= |
|||
In {{incode|create()}}, change the following line: |
|||
Part.show(Part.makeBox(obj.Length, obj.Width, obj.Height)) |
|||
}} |
|||
obj = App.ActiveDocument.addObject('App::FeaturePython', obj_name) |
|||
to read:<br> |
|||
obj = App.ActiveDocument.addObject('<span style="color:red;">Part::FeaturePython</span>', obj_name) |
|||
To finish making the changes we need, the following line in the {{incode|execute()}} method needs to be changed: |
|||
Part.show(Part.makeBox(obj.Length, obj.Width, obj.Height)) |
|||
to: |
to: |
||
obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height) |
|||
{{Code|code= |
|||
Your code should look like this: |
|||
obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height) |
|||
}} |
|||
import FreeCAD as App |
|||
import Part |
|||
def create(obj_name): |
|||
""" |
|||
Object creation method |
|||
""" |
|||
obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name) |
|||
fpo = box(obj) |
|||
App.ActiveDocument.recompute() |
|||
return fpo |
|||
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 |
|||
self.Object = obj |
|||
def execute(self, obj): |
|||
""" |
|||
Called on document recompute |
|||
""" |
|||
obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height) |
|||
[[File:Part_featurepython_no_vp.png|right]] |
[[File:Part_featurepython_no_vp.png|right]] |
||
Now, save your changes and switch back to FreeCAD. Delete any existing objects, reload the box module, and create a new box object. |
|||
Save your changes, switch back to FreeCAD, delete any existing objects, reload the box module, and create a new box object. The new result is somewhat disappointing. There no longer is an extra object in the Tree view, and the icon in the Tree view has changed, but our box in the 3D view is also gone (which is why the icon is gray). What happened? Although we've properly created our box shape and assigned it to a {{incode|Part::FeaturePython}} object, to make it actually show up in the 3D view we need to assign a [[Viewprovider|ViewProvider]]. |
|||
The results may seem a bit mixed. The icon in the treeview is different - it's a box, now. But there's no cube. And the icon is still gray! |
|||
{{clear}} |
|||
[[#top|top]] |
|||
What happened? Although we've properly created our box shape and assigned it to a {{incode|Part::FeaturePython}} object, before we can make it show up in our 3D view, we need to assign a '''ViewProvider.''' |
|||
== |
==Writing a ViewProvider== |
||
A View Provider is the component of an object which allows it to have visual representation |
A View Provider is the component of an object which allows it to have a visual representation in the 3D view. FreeCAD uses an application structure which is designed to separate the data (the "model") from it's visual representation (the "view"). If you've spent any time working with FreeCAD in Python, you are likely already aware of this through the use of the two core Python modules: {{incode|FreeCAD}} and {{incode|FreeCADGui}} (often aliased as {{incode|App}} and {{incode|Gui}} repectively). |
||
Our FeaturePython object also requires these elements. Thus far we've focused purely on the "model" portion of the code, now it's time to write the "view" portion. Fortunately most ViewProviders are simple and require little effort to write, at least to get started. Here's an example ViewProvider borrowed and slightly modified from [https://github.com/FreeCAD/FreeCAD/blob/master/src/Mod/TemplatePyMod/FeaturePython.py]: |
|||
{{Code|code= |
|||
class ViewProviderBox: |
|||
class ViewProviderBox: |
|||
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.Proxy = self |
obj.Proxy = self |
||
def attach(self, obj): |
def attach(self, obj): |
||
""" |
""" |
||
Line 130: | Line 96: | ||
""" |
""" |
||
return |
return |
||
def updateData(self, fp, prop): |
def updateData(self, fp, prop): |
||
""" |
""" |
||
Line 136: | Line 102: | ||
""" |
""" |
||
return |
return |
||
def getDisplayModes(self,obj): |
def getDisplayModes(self,obj): |
||
""" |
""" |
||
Return a list of display modes. |
Return a list of display modes. |
||
""" |
""" |
||
return |
return [] |
||
def getDefaultDisplayMode(self): |
def getDefaultDisplayMode(self): |
||
""" |
""" |
||
Line 148: | Line 114: | ||
""" |
""" |
||
return "Shaded" |
return "Shaded" |
||
def setDisplayMode(self,mode): |
def setDisplayMode(self,mode): |
||
""" |
""" |
||
Line 156: | Line 122: | ||
""" |
""" |
||
return mode |
return mode |
||
def onChanged(self, vp, prop): |
def onChanged(self, vp, prop): |
||
""" |
""" |
||
Print the name of the property that has changed |
Print the name of the property that has changed |
||
""" |
""" |
||
App.Console.PrintMessage("Change property: " + str(prop) + "\n") |
App.Console.PrintMessage("Change property: " + str(prop) + "\n") |
||
def getIcon(self): |
def getIcon(self): |
||
""" |
""" |
||
Return the icon in XMP format which will appear in the tree view. This method is optional and if not defined a default icon is shown. |
Return the icon in XMP format which will appear in the tree view. This method is optional and if not defined a default icon is shown. |
||
""" |
""" |
||
return """ |
return """ |
||
/* XPM */ |
/* XPM */ |
||
static const char * ViewProviderBox_xpm[] = { |
static const char * ViewProviderBox_xpm[] = { |
||
"16 16 6 1", |
"16 16 6 1", |
||
" |
" c None", |
||
". |
". c #141010", |
||
"+ |
"+ c #615BD2", |
||
"@ |
"@ c #C39D55", |
||
"# |
"# c #000000", |
||
"$ |
"$ c #57C355", |
||
" ........", |
" ........", |
||
" ......++..+..", |
" ......++..+..", |
||
Line 196: | Line 162: | ||
" ####### "}; |
" ####### "}; |
||
""" |
""" |
||
def |
def dumps(self): |
||
""" |
""" |
||
Called during document saving. |
Called during document saving. |
||
""" |
""" |
||
return None |
return None |
||
def |
def loads(self,state): |
||
""" |
|||
Called during document restore. |
Called during document restore. |
||
""" |
|||
return None |
return None |
||
}} |
|||
In the code above, we define an XMP icon for this object. Icon design is beyond the scope of this tutorial, but basic design can be managed using open source tools like [https://www.gimp.org GIMP], [https://krita.org/en/ Krita], and [https://inkscape.org/ Inkscape]. The {{incode|getIcon()}} method is optional, FreeCAD will use a default icon if this method is not provided. |
|||
Add the ViewProvider code at the end of {{FileName|box.py}} and in the {{incode|create()}} method insert the following line above the {{incode|recompute()}} statement: |
|||
{{Code|code= |
|||
With out ViewProvider defined, we now need to put it to use to give our object the gift of visualization. |
|||
ViewProviderBox(obj.ViewObject) |
|||
}} |
|||
This instances the custom ViewProvider class and passes the FeaturePython's built-in ViewObject to it. When the ViewProvider class initializes, it saves a reference to itself in the FeaturePython's {{incode|ViewObject.Proxy}} attribute. This way, when FreeCAD needs to render our box visually, it can find the ViewProvider class to do that. |
|||
Return to the <code>create()</code> method in your code and add the following near the end: |
|||
Now, save the changes and return to FreeCAD. Import or reload the box module and call {{incode|box.create()}}. You should now see two things: |
|||
ViewProviderBox(obj.ViewObject) |
|||
*The icon for the box object has changed. |
|||
*And, more importantly, there is a box in the 3D view. If you do not see it press the {{Button|[[Image:Std_ViewFitAll.svg|16px]] [[Std_ViewFitAll|Std ViewFitAll]]}} button. You can even alter the dimensions of the box by changing the values in the [[Property_editor|Property editor]]. Give it a try! |
|||
[[#top|top]] |
|||
This instances the custom ViewProvider class and passes the FeaturePython's built-in ViewObject to it. The ViewObject won't do anything without our custom class implementation, so when the ViewProvider class initializes, it saves a reference to itself in the FeaturePython's ViewObject.Proxy attribute. This way, when FreeCAD needs to render our Box visually, it can find the ViewProvider class to do that. |
|||
==Trapping events== |
|||
Now, save the changes and return to FreeCAD. Import or reload the Box module and call <code>box.create()</code>. |
|||
We have already discussed event trapping. Nearly every method of a FeaturePython class serves as a callback accessible to the FeaturePython object (which gets access to our class instance through the {{incode|Proxy}} attribute). |
|||
We still don't see anything, but notice what happened to the icon next to the box object. It's in color! And it has a shape! That's a clue that our ViewProvider is working as expected. |
|||
So now, it's time to actually *add* a box. |
|||
Return to the code and add the following line to the execute() method: |
|||
obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height) |
|||
Now save, reload the box module in FreeCAD and create a new box. |
|||
You should see a box appear on screen! If it's too big (or maybe doesn't show up at all), click one of the 'ViewFit' buttons to fit it to the 3D view. |
|||
Note what we did here that differs from the way it was implemented for App::FeaturePython above. In the <code>execute()</code> method, we called the <code>Part.makeBox()</code> macro and passed it the box properties as before. But rather than call <code>Part.show()</code> on the resulting object (which created a new, separate box object), we simply assigned it to the <code>Shape</code> property of our Part::FeaturePython object instead. |
|||
You can even alter the box dimensions by changing the values in the FreeCAD property panel. Give it a try! |
|||
=== Trapping Events === |
|||
To this point, we haven't explicitly addressed event trapping. Nearly every method of a FeaturePython class serves as a callback accessible to the FeaturePython object (which gets access to our class instance through the <code>Proxy</code> attribute, if you recall). |
|||
Below is a list of the callbacks that may be implemented in the basic FeaturePython object: |
Below is a list of the callbacks that may be implemented in the basic FeaturePython object: |
||
{| class="wikitable" |
{| class="wikitable" cellpadding="5px" width="100%" |
||
|+ |
|+ FeaturePython basic callbacks |
||
|style="width:25%" | {{incode|execute(self, obj)}} |
|||
|<code>execute(self, obj)</code> || Called during document recomputes || Do not call <code>recompute()</code> from this method (or any method called from <code>execute()</code>) as this causes a nested recompute. |
|||
|style="width:25%" | Called during document recomputes |
|||
|style="width:50%" | Do not call {{incode|recompute()}} from this method (or any method called from {{incode|execute()}}) as this causes a nested recompute. |
|||
|- |
|- |
||
| {{incode|onBeforeChange(self, obj, prop)}} |
|||
|<code>onBeforeChanged(self, obj, prop)</code> || Called before a property value is changed || <code>prop</code> is the name of the property to be changed, not the property object itself. Property changes cannot be cancelled. Previous / next property values are not simultaneously available for comparison. |
|||
| Called before a property value is changed |
|||
| {{incode|prop}} is the name of the property to be changed, not the property object itself. Property changes cannot be cancelled. Previous / next property values are not simultaneously available for comparison. |
|||
|- |
|- |
||
| {{incode|onChanged(self, obj, prop)}} |
|||
|<code>onChanged(self, obj, prop)</code> || Called after a property is changed || <code>prop</code> is the name of the property to be changed, not the property object itself. |
|||
| Called after a property is changed |
|||
| {{incode|prop}} is the name of the property to be changed, not the property object itself. |
|||
|- |
|- |
||
| {{Incode|onDocumentRestored(self, obj)}} |
|||
|{{Incode|onDocumentRestored(self, obj)}} || Called after a document is restored or aFeaturePython object is copied / duplicated. || Occasionally, references to the FeaturePython object from the class, or the class from the FeaturePython object may be broken, as the class <code>__init__()</code> method is not called when the object is reconstructed. Adding <code>self.Object = obj</code> or <code>obj.Proxy = self</code> often solves these issues. |
|||
| Called after a document is restored or a FeaturePython object is copied. |
|||
| Occasionally, references to the FeaturePython object from the class, or the class from the FeaturePython object may be broken, as the class {{incode|__init__()}} method is not called when the object is reconstructed. Adding {{incode|self.Object <nowiki>=</nowiki> obj}} or {{incode|obj.Proxy <nowiki>=</nowiki> self}} often solves these issues. |
|||
|} |
|} |
||
For a complete reference of FeaturePython methods available, see [[FeaturePython_methods|FeaturePython methods]]. |
|||
In addition, there are two callbacks in the ViewProvider class that may occasionally prove useful: |
In addition, there are two callbacks in the ViewProvider class that may occasionally prove useful: |
||
{| class="wikitable" |
{| class="wikitable" cellpadding="5px" width="100%" |
||
|+ |
|+ ViewProvider basic callbacks |
||
|- |
|- |
||
|style="width:25%" | {{incode|updateData(self, obj, prop)}} |
|||
|<code>updateData(self, obj, prop)</code> || Called after a data (model) property is changed || <code>obj</code> is a reference to the FeaturePython class instance, not the ViewProvider instance. <code>prop</code> is the name of the property to be changed, not the property object itself. |
|||
|style="width:25%" | Called after a data (model) property is changed |
|||
|style="width:50%" | {{incode|obj}} is a reference to the FeaturePython class instance, not the ViewProvider instance. {{incode|prop}} is the name of the property to be changed, not the property object itself. |
|||
|- |
|- |
||
| {{incode|onChanged(self, vobj, prop)}} |
|||
|<code>onChanged(self, vobj, prop)</code> || Called after a view property is changed || <code>vobj</code> is a reference to the ViewProvider instance. <code>prop</code> is the name of the view property which was changed. |
|||
| Called after a view property is changed |
|||
| {{incode|vobj}} is a reference to the ViewProvider instance. {{incode|prop}} is the name of the view property which was changed. |
|||
|} |
|} |
||
It is not uncommon to encounter a situation where the Python callbacks are not being triggered as they should. Beginners in this area can rest assured that the FeaturePython callback system is not fragile or broken. Invariably when callbacks fail to run it is because a reference is lost or undefined in the underlying code. If, however, callbacks appear to be breaking with no explanation, providing object/proxy references in the {{incode|onDocumentRestored()}} callback (as noted in the first table above) may alleviate these problems. Until you are comfortable with the callback system, it may be useful to add print statements in each callback to print messages to the console during development. |
|||
[[#top|top]] |
|||
{| class="wikitable" |
|||
!Tip |
|||
|- |
|||
|It is not uncommon to encounter a situation where the Python callbacks are not being triggered as they should. Beginners in this area need to rest assured that the FeaturePython callback system is not fragile or broken. Invariably, when callbacks fail to run, it is because a reference is lost or undefined in the underlying code. If, however, callbacks appear to be breaking with no explanation, providing object / proxy references in the <code>onDocumentRestored()</code> callback (as noted in the first table above) may alleviate these problems. Until you are comfortable with the callback system, it may be useful to add print statements in each callback to print messages to the console as a diagnostic during development. |
|||
|} |
|||
== |
==Complete code== |
||
{{Code|code= |
|||
The complete code for this example: |
|||
import FreeCAD as App |
|||
import Part |
|||
def create(obj_name): |
|||
""" |
""" |
||
Object creation method |
Object creation method |
||
""" |
""" |
||
obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name) |
obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name) |
||
box(obj) |
|||
ViewProviderBox(obj.ViewObject) |
ViewProviderBox(obj.ViewObject) |
||
App.ActiveDocument.recompute() |
App.ActiveDocument.recompute() |
||
return |
return obj |
||
class box(): |
|||
class box(): |
|||
def __init__(self, obj): |
def __init__(self, obj): |
||
""" |
""" |
||
Default |
Default constructor |
||
""" |
""" |
||
self.Type = 'box' |
self.Type = 'box' |
||
self.ratios = None |
|||
obj.addProperty('App::PropertyString', 'Description', 'Base', 'Box description').Description = 'Hello World!' |
|||
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.addProperty('App::PropertyBool', 'Aspect Ratio', 'Dimensions', 'Lock the box aspect ratio').Aspect_Ratio = False |
|||
obj.Proxy = self |
obj.Proxy = self |
||
self.Object = obj |
|||
obj.addProperty('App::PropertyString', 'Description', 'Base', 'Box description').Description = "" |
|||
obj.addProperty('App::PropertyLength', 'Length', 'Dimensions', 'Box length').Length = 10.0 |
|||
def __getstate__(self): |
|||
obj.addProperty('App::PropertyLength', 'Width', 'Dimensions', 'Box width').Width = '10 mm' |
|||
return self.Type |
|||
obj.addProperty('App::PropertyLength', 'Height', 'Dimensions', 'Box height').Height = '1 cm' |
|||
def __setstate__(self, state): |
|||
if state: |
|||
self.Type = state |
|||
def execute(self, obj): |
def execute(self, obj): |
||
""" |
""" |
||
Called on document recompute |
Called on document recompute |
||
""" |
""" |
||
obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height) |
obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height) |
||
class ViewProviderBox: |
|||
class ViewProviderBox: |
|||
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.Proxy = self |
obj.Proxy = self |
||
def attach(self, obj): |
def attach(self, obj): |
||
""" |
""" |
||
Line 341: | Line 295: | ||
""" |
""" |
||
return |
return |
||
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 |
||
""" |
""" |
||
return |
return |
||
def getDisplayModes(self,obj): |
def getDisplayModes(self,obj): |
||
""" |
""" |
||
Return a list of display modes. |
Return a list of display modes. |
||
""" |
""" |
||
return |
return [] |
||
def getDefaultDisplayMode(self): |
def getDefaultDisplayMode(self): |
||
""" |
""" |
||
Line 360: | Line 313: | ||
""" |
""" |
||
return "Shaded" |
return "Shaded" |
||
def setDisplayMode(self,mode): |
def setDisplayMode(self,mode): |
||
""" |
""" |
||
Line 368: | Line 321: | ||
""" |
""" |
||
return mode |
return mode |
||
def onChanged(self, |
def onChanged(self, vp, prop): |
||
""" |
""" |
||
Print the name of the property that has changed |
Print the name of the property that has changed |
||
""" |
""" |
||
App.Console.PrintMessage("Change property: " + str(prop) + "\n") |
App.Console.PrintMessage("Change property: " + str(prop) + "\n") |
||
def getIcon(self): |
def getIcon(self): |
||
""" |
""" |
||
Return the icon in XMP format which will appear in the tree view. This method is optional and if not defined a default icon is shown. |
Return the icon in XMP format which will appear in the tree view. This method is optional and if not defined a default icon is shown. |
||
""" |
""" |
||
return """ |
return """ |
||
/* XPM */ |
/* XPM */ |
||
static const char * ViewProviderBox_xpm[] = { |
static const char * ViewProviderBox_xpm[] = { |
||
"16 16 6 1", |
"16 16 6 1", |
||
" |
" c None", |
||
". |
". c #141010", |
||
"+ |
"+ c #615BD2", |
||
"@ |
"@ c #C39D55", |
||
"# |
"# c #000000", |
||
"$ |
"$ c #57C355", |
||
" ........", |
" ........", |
||
" ......++..+..", |
" ......++..+..", |
||
Line 408: | Line 361: | ||
" ####### "}; |
" ####### "}; |
||
""" |
""" |
||
def |
def dumps(self): |
||
""" |
|||
Called during document saving. |
|||
""" |
|||
return None |
return None |
||
def |
def loads(self,state): |
||
""" |
|||
Called during document restore. |
|||
""" |
|||
return None |
return None |
||
}} |
|||
{{docnav|FeaturePython Objects|Scripting examples}} |
|||
[[#top|top]] |
|||
{{Userdocnavi}} |
|||
{{Docnav/it |
|||
[[Category:Tutorials]] |
|||
|[[Create_a_FeaturePython_object_part_I/it|Create a FeaturePython object part I]] |
|||
| |
|||
[[Category:Python Code]] |
|||
|IconL= |
|||
|IconR= |
|||
}} |
|||
{{Powerdocnavi{{#translation:}}}} |
|||
[[Category:Poweruser Documentation]] |
|||
[[Category:Developer Documentation{{#translation:}}]] |
|||
[[Category:Python Code{{#translation:}}]] |
|||
{{clear}} |
Latest revision as of 09:00, 27 January 2024
Introduzione
On the Create a FeaturePython object part I page we've focused on the internal aspects of a Python class built around a FeaturePython object, specifically an App::FeaturePython
object. We've created the object, defined some properties, and added a document-level event callback that allows our object to respond to a document recompute with the execute()
method. But our object still lacks a presence in the 3D view. Let's remedy that by adding a box.
Adding a box
First at the top of the box.py file, below the existing import, add:
import Part
Then in execute()
delete the print()
statement and add the following line in its place:
Part.show(Part.makeBox(obj.Length, obj.Width, obj.Height))
![](/images/b/bf/App_featurepython_box.png)
These commands execute Python methods that come with FreeCAD by default:
- The
Part.makeBox()
method generates a box shape. - The enclosing call to
Part.show()
adds the shape to the document and makes it visible.
Delete any existing objects, reload the box module and create a new box object using box.create()
. Notice how a box immediately appears on the screen. That is because we force the document to recompute at the end of box.create()
which in turn triggers the execute()
method of our box
class.
At first glance the result may look fine but there are some problems. The most obvious one is that the box is represented by an entirely different object than our FeaturePython object. Part.show()
simply creates a separate box object and adds it to the document. Worse, if you change the dimensions of the FeaturePython object another box shape gets created, and the old one is left in place. And if you have the Report view open, you may have noticed an error stating "Recursive calling of recompute for document Unnamed". This has to do with using the Part.show()
method inside a FeaturePython object.
Fixing the code
To solve these problems we have to make a number of changes. Until now we've been using a App::FeaturePython
object which is actually not intended to have a visual representation in the 3D view. We have to use a Part::FeaturePython
object instead. In create()
change the following line:
obj = App.ActiveDocument.addObject('App::FeaturePython', obj_name)
to:
obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name)
To get rid of the separate box object we need to assigns the result of the makeBox()
method to the Shape
property of our Part::FeaturePython
object. Change this line in execute()
:
Part.show(Part.makeBox(obj.Length, obj.Width, obj.Height))
to:
obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height)
![](/images/5/5f/Part_featurepython_no_vp.png)
Save your changes, switch back to FreeCAD, delete any existing objects, reload the box module, and create a new box object. The new result is somewhat disappointing. There no longer is an extra object in the Tree view, and the icon in the Tree view has changed, but our box in the 3D view is also gone (which is why the icon is gray). What happened? Although we've properly created our box shape and assigned it to a Part::FeaturePython
object, to make it actually show up in the 3D view we need to assign a ViewProvider.
Writing a ViewProvider
A View Provider is the component of an object which allows it to have a visual representation in the 3D view. FreeCAD uses an application structure which is designed to separate the data (the "model") from it's visual representation (the "view"). If you've spent any time working with FreeCAD in Python, you are likely already aware of this through the use of the two core Python modules: FreeCAD
and FreeCADGui
(often aliased as App
and Gui
repectively).
Our FeaturePython object also requires these elements. Thus far we've focused purely on the "model" portion of the code, now it's time to write the "view" portion. Fortunately most ViewProviders are simple and require little effort to write, at least to get started. Here's an example ViewProvider borrowed and slightly modified from [1]:
class ViewProviderBox:
def __init__(self, obj):
"""
Set this object to the proxy object of the actual view provider
"""
obj.Proxy = self
def attach(self, obj):
"""
Setup the scene sub-graph of the view provider, this method is mandatory
"""
return
def updateData(self, fp, prop):
"""
If a property of the handled feature has changed we have the chance to handle this here
"""
return
def getDisplayModes(self,obj):
"""
Return a list of display modes.
"""
return []
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):
"""
Print the name of the property that has changed
"""
App.Console.PrintMessage("Change property: " + str(prop) + "\n")
def getIcon(self):
"""
Return the icon in XMP 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):
"""
Called during document saving.
"""
return None
def loads(self,state):
"""
Called during document restore.
"""
return None
In the code above, we define an XMP icon for this object. Icon design is beyond the scope of this tutorial, but basic design can be managed using open source tools like GIMP, Krita, and Inkscape. The getIcon()
method is optional, FreeCAD will use a default icon if this method is not provided.
Add the ViewProvider code at the end of box.py and in the create()
method insert the following line above the recompute()
statement:
ViewProviderBox(obj.ViewObject)
This instances the custom ViewProvider class and passes the FeaturePython's built-in ViewObject to it. When the ViewProvider class initializes, it saves a reference to itself in the FeaturePython's ViewObject.Proxy
attribute. This way, when FreeCAD needs to render our box visually, it can find the ViewProvider class to do that.
Now, save the changes and return to FreeCAD. Import or reload the box module and call box.create()
. You should now see two things:
- The icon for the box object has changed.
- And, more importantly, there is a box in the 3D view. If you do not see it press the
Std ViewFitAll button. You can even alter the dimensions of the box by changing the values in the Property editor. Give it a try!
Trapping events
We have already discussed event trapping. Nearly every method of a FeaturePython class serves as a callback accessible to the FeaturePython object (which gets access to our class instance through the Proxy
attribute).
Below is a list of the callbacks that may be implemented in the basic FeaturePython object:
execute(self, obj)
|
Called during document recomputes | Do not call recompute() from this method (or any method called from execute() ) as this causes a nested recompute.
|
onBeforeChange(self, obj, prop)
|
Called before a property value is changed | prop is the name of the property to be changed, not the property object itself. Property changes cannot be cancelled. Previous / next property values are not simultaneously available for comparison.
|
onChanged(self, obj, prop)
|
Called after a property is changed | prop is the name of the property to be changed, not the property object itself.
|
onDocumentRestored(self, obj)
|
Called after a document is restored or a FeaturePython object is copied. | Occasionally, references to the FeaturePython object from the class, or the class from the FeaturePython object may be broken, as the class __init__() method is not called when the object is reconstructed. Adding self.Object = obj or obj.Proxy = self often solves these issues.
|
For a complete reference of FeaturePython methods available, see FeaturePython methods.
In addition, there are two callbacks in the ViewProvider class that may occasionally prove useful:
updateData(self, obj, prop)
|
Called after a data (model) property is changed | obj is a reference to the FeaturePython class instance, not the ViewProvider instance. prop is the name of the property to be changed, not the property object itself.
|
onChanged(self, vobj, prop)
|
Called after a view property is changed | vobj is a reference to the ViewProvider instance. prop is the name of the view property which was changed.
|
It is not uncommon to encounter a situation where the Python callbacks are not being triggered as they should. Beginners in this area can rest assured that the FeaturePython callback system is not fragile or broken. Invariably when callbacks fail to run it is because a reference is lost or undefined in the underlying code. If, however, callbacks appear to be breaking with no explanation, providing object/proxy references in the onDocumentRestored()
callback (as noted in the first table above) may alleviate these problems. Until you are comfortable with the callback system, it may be useful to add print statements in each callback to print messages to the console during development.
Complete code
import FreeCAD as App
import Part
def create(obj_name):
"""
Object creation method
"""
obj = App.ActiveDocument.addObject('Part::FeaturePython', obj_name)
box(obj)
ViewProviderBox(obj.ViewObject)
App.ActiveDocument.recompute()
return obj
class box():
def __init__(self, obj):
"""
Default constructor
"""
self.Type = 'box'
obj.Proxy = self
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'
def execute(self, obj):
"""
Called on document recompute
"""
obj.Shape = Part.makeBox(obj.Length, obj.Width, obj.Height)
class ViewProviderBox:
def __init__(self, obj):
"""
Set this object to the proxy object of the actual view provider
"""
obj.Proxy = self
def attach(self, obj):
"""
Setup the scene sub-graph of the view provider, this method is mandatory
"""
return
def updateData(self, fp, prop):
"""
If a property of the handled feature has changed we have the chance to handle this here
"""
return
def getDisplayModes(self,obj):
"""
Return a list of display modes.
"""
return []
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):
"""
Print the name of the property that has changed
"""
App.Console.PrintMessage("Change property: " + str(prop) + "\n")
def getIcon(self):
"""
Return the icon in XMP 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):
"""
Called during document saving.
"""
return None
def loads(self,state):
"""
Called during document restore.
"""
return None
![](/images/thumb/8/8f/Power_user_hub.png/24px-Power_user_hub.png)
- FreeCAD scripting: Python, Introduction to Python, Python scripting tutorial, FreeCAD Scripting Basics
- Modules: Builtin modules, Units, Quantity
- Workbenches: Workbench creation, Gui Commands, Commands, Installing more workbenches
- Meshes and Parts: Mesh Scripting, Topological data scripting, Mesh to Part, PythonOCC
- Parametric objects: Scripted objects, Viewproviders (Custom icon in tree view)
- Scenegraph: Coin (Inventor) scenegraph, Pivy
- Graphical interface: Interface creation, Interface creation completely in Python (1, 2, 3, 4, 5), PySide, PySide examples beginner, intermediate, advanced
- Macros: Macros, How to install macros
- Embedding: Embedding FreeCAD, Embedding FreeCADGui
- Other: Expressions, Code snippets, Line drawing function, FreeCAD vector math library (deprecated)
- Hubs: User hub, Power users hub, Developer hub