feature python obj: init() - execute() - onChanged()

Need help, or want to share a macro? Post here!
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
Post Reply
karim.achaibou
Posts: 62
Joined: Tue Oct 26, 2021 5:39 pm

feature python obj: init() - execute() - onChanged()

Post by karim.achaibou »

Hello,

Below, code of a simplified object to better understand what happens when creating a feature python object.
I have several question when executing this program.

Code: Select all

import FreeCAD as App
import FreeCADGui as Gui
import Part

def create(obj_name="TubeCompound"):
    obj = App.ActiveDocument.addObject("Part::FeaturePython", "tube_1")
    Tubetest(obj, 1000, 150)
    obj.ViewObject.Proxy = 0
    print("\n-> call to App.activeDocument().recompute()")
    App.activeDocument().recompute()

class Tubetest:
    def __init__(self, obj, length=1000, dia=150):
        print("\n-> in init tubetest")
        self.Type = "Tube"
        obj.Proxy = self
        self.Name = obj.Name

        obj.addProperty('App::PropertyLength', 'length', 'Afmetingen', 'buis lengte').length = length
        obj.addProperty('App::PropertyLength', 'dia', 'Afmetingen', 'buis buitendiameter').dia = dia

        # obj.recompute()

    def execute(self, obj):
        print("\n-> in execute tubetest")
        obj.Shape = Part.makeCylinder(obj.dia/2, obj.length)

    def onChanged(self, obj, prop):
        #https://forum.freecad.org/viewtopic.php?style=4&t=48182
        print("\n-> Hello, I am a Tubetest object : {}".format(self))
        print("My onChanged method is currently executed")
        print("because the property {}".format(prop))
        print("of the FeaturePython Object {}".format(obj.Label))
        print("has been changed to {}".format(obj.getPropertyByName(prop)))
        print("and this FeaturePython Object Proxy points to me : {}".format(obj.getPropertyByName("Proxy")))
output:

Code: Select all

>>> test2.create()

-> in init tubetest

-> Hello, I am a Tubetest object : <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>
My onChanged method is currently executed
because the property Proxy
of the FeaturePython Object tube_1
has been changed to <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>
and this FeaturePython Object Proxy points to me : <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>

-> Hello, I am a Tubetest object : <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>
My onChanged method is currently executed
because the property length
of the FeaturePython Object tube_1
has been changed to 1000.0 mm
and this FeaturePython Object Proxy points to me : <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>

-> Hello, I am a Tubetest object : <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>
My onChanged method is currently executed
because the property dia
of the FeaturePython Object tube_1
has been changed to 150.0 mm
and this FeaturePython Object Proxy points to me : <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>

-> call to App.activeDocument().recompute()

-> in execute tubetest

-> Hello, I am a Tubetest object : <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>
My onChanged method is currently executed
because the property Shape
of the FeaturePython Object tube_1
has been changed to <Solid object at 0000025F13B002E0>
and this FeaturePython Object Proxy points to me : <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>
1.When you create your object and assign it to the class, all properties are initialized and therefore there values gets changed which automatically calls the onChanged() function.

Text in Italic I left for reference because this is not the correct way of updating the object but what I do all the time. This should be handled by the document, therefore I commented out "obj.recompute()" in the __init__ function and added "App.activeDocument().recompute()" in the create() methode.
***
In the __init__() method I recompute the object (obj.recompute()) so the execute() function is called and the Shape is created. Because this Shape is assigned to the Shape property the onChanged() function is called and therefore the object is still touched - I think, is this correct?
To resolve this you have to recompute the object and then the object is ['Up-to-date']. This looks strange because a new Shape solid is created which calls again the onChanged() function.
Is this normal behavior? Can this be avoided that the execute() functions gets called two times?

***

2.Do all the properties have to be initialized in the __init__() function or may they also be defined and assigned in the execute() method? I don't think they all should be assigned in the __init__() function, because the Shape property is assigned in the execute() method and all my other code which assigns values to properties works fine. Is this correct and what is the best practice?

3. What to do in execute() and what in the onChanged() function?
- execute(): only the creation of the Shape and/or everything else also (see also point 2) (eg. calculate properties, create and assign other sub shapes which then can be used by other objects ...)
- onChanged(): handling when properties change in the GUI? I see that the execute() method is also called when changing a property in the GUI (as read elsewhere this should and is handled by the document it self).

Below is the output when changing property length to 1mm.

Code: Select all

-> Hello, I am a Tubetest object : <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>
13:48:25  My onChanged method is currently executed
13:48:25  because the property length
13:48:25  of the FeaturePython Object tube_1
13:48:25  has been changed to 1.0 mm
13:48:25  and this FeaturePython Object Proxy points to me : <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>
13:48:30  
-> in execute tubetest
13:48:30  
-> Hello, I am a Tubetest object : <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>
13:48:30  My onChanged method is currently executed
13:48:30  because the property Shape
13:48:30  of the FeaturePython Object tube_1
13:48:30  has been changed to <Solid object at 0000025F09EEBB80>
13:48:30  and this FeaturePython Object Proxy points to me : <fpo.tube.test2.Tubetest object at 0x0000025F0AEF05E0>
For now I use the OnChanged() method as below (update the position of a child object if Shape of my object changes and the value of my child object has an other value than an other child objects property.)
Do you have to use the onChanged() method for the object it self If it hasn't any child objects you wish to update, like code below? When to use it?

Code: Select all

    def onChanged(self, obj, prop):
        App.Console.PrintMessage("Change property: " + str(obj.Name) + ": " + str(prop) + "\n")
        if prop == "Shape" and obj.Links[2].Placement.Base != App.Vector(0, 0, obj.Links[1].Hoogtebuis):
            obj.Links[2].Placement.Base = App.Vector(0, 0, obj.Links[1].Hoogtebuis)
Just by writing this topic I already have a better inside, but I believe this post will help a lot of others who starts writing code in FreeCAD.

thanks in advance for the elaborated feedback and new insides !!
karim.achaibou
Posts: 62
Joined: Tue Oct 26, 2021 5:39 pm

Re: feature python obj: init() - execute() - onChanged()

Post by karim.achaibou »

  1. 1.When you create your object and assign it to the class, all properties are initialized and therefore there values gets changed which automatically calls the onChanged() function. ... correct way of updating the object ...
    Should be handled by the document --> App.activeDocument().recompute()
    new Question, when then to use obj.recompute()
  • 2.Do all the properties have to be initialized in the __init__() function or may they also be defined and assigned in the execute() method? I don't think they all should be assigned in the __init__() function, because the Shape property is assigned in the execute() method and all my other code which assigns values to properties works fine. Is this correct and what is the best practice?
    No
  • 3. What to do in execute() and what in the onChanged() function?
    - execute(): only the creation of the Shape and/or everything else also (see also point 2) (eg. calculate properties, create and assign other sub shapes which then can be used by other objects ...)
    - onChanged(): handling when properties change in the GUI? I see that the execute() method is also called when changing a property in the GUI (as read elsewhere this should and is handled by the document it self).
    Some insides would be appreciated
User avatar
onekk
Veteran
Posts: 6222
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: feature python obj: init() - execute() - onChanged()

Post by onekk »

karim.achaibou wrote: Sat May 13, 2023 11:09 am ...
It depends

It is left to the programmer, you could avoid as example to run the on changed function setting a flag in the init that will trigger an early return of onChanged.

this way when you add properties onChanged os called but a simple check could make onChanged do nothing.

once the init phase is done you unset the flag and onChanged will behave as expected.

Regards

Carlo D.
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
karim.achaibou
Posts: 62
Joined: Tue Oct 26, 2021 5:39 pm

Re: feature python obj: init() - execute() - onChanged()

Post by karim.achaibou »

Thanks for your feedback Carlo
onekk wrote: Sat May 13, 2023 1:28 pm you could avoid as example to run the on changed function setting a flag in the init that will trigger an early return of onChanged.
So in the init method you declare a new property

Code: Select all

self.flagInit = True
# add properties 
self.flagInit = False
In the onChanged methode

Code: Select all

if self.flagInit:
    return
User avatar
onekk
Veteran
Posts: 6222
Joined: Sat Jan 17, 2015 7:48 am
Contact:

Re: feature python obj: init() - execute() - onChanged()

Post by onekk »

karim.achaibou wrote: Fri May 19, 2023 8:36 am Thanks for your feedback Carlo
...
Yes exactly
GitHub page: https://github.com/onekk/freecad-doc.
- In deep articles on FreeCAD.
- Learning how to model with scripting.
- Various other stuffs.

Blog: https://okkmkblog.wordpress.com/
edi
Posts: 482
Joined: Fri Jan 17, 2020 1:32 pm

Re: feature python obj: init() - execute() - onChanged()

Post by edi »

The framework of FreeCAD is organized simple:
- The main container is the document. You create it typing:

Code: Select all

>>> doc = FreeCAD.newDocument()
in the Python console.
- The document contains several document-objects. Each document-object defines several properties. A document-object can be added typing:

Code: Select all

>>> obj = doc.addObject(type,name)
The two parameters are strings. There are several types. All types look like "Part::Box". This is a C++ used naming convention.
If you type:

Code: Select all

>>> myBox = doc.addObject("Part::Box","myBox")
a new box is created. You can display its properties typing:

Code: Select all

>>> myBox.PropertiesList
It has several properies, among them Height, Length, Width, and Shape.
If you type:

Code: Select all

>>> myFeature = doc.addObject("Part::FeaturePython","myFeature")
a new feature is created, having none of the above properties. The feature has no shape, and no geometry describing Properties. But it has a property "Proxy", which can hold the address of an instance of a class.

You have to create a new class (in the below example named "MyCube"), which must have a constructor and the two methods execute and onChanged:

- The constructor connects the instance of "MyCube" (self) to the Property "Proxy" of the Part::FeaturePython object. FreeCAD needs this, to perform the call-back mechanism. It also defines a geometrical Property "edgeLength" and presets it with a default value. Usually all properties are defined and initialized here.

- The method execute is called by FreeCAD using a call-back mechanism if the geometry has to be created. It defines the property "Shape" of the Part::FeaturePython object using the actual values of the properties.

-The method onChanged is called by FreeCAD using a call-back mechanism if any of the properties have been changed in the Combo-View.

Find attached a minimum example creating a cube using the length of its edge as property:

Code: Select all

import Part
class MyCube:
    def __init__(self,MyCube):
        MyCube.Proxy = self
        MyCube.addProperty('App::PropertyLength','edgeLength').edgeLength = 40
    def execute(self,MyCube):
        edgeLength = MyCube.edgeLength
        MyCube.Shape = Part.makeBox(edgeLength,edgeLength,edgeLength)
    def onChanged(self,MyCube,Parameter):
        if Parameter == 'edgeLength':
            self.execute(MyCube)

myFeature = FreeCAD.ActiveDocument.addObject('Part::FeaturePython','myFeature')
MyCube(myFeature) # create the instance of the class defining the properties and the shape
myFeature.ViewObject.Proxy = 0 # use the default viewprovider
FreeCAD.ActiveDocument.recompute()
karim.achaibou
Posts: 62
Joined: Tue Oct 26, 2021 5:39 pm

Re: feature python obj: init() - execute() - onChanged()

Post by karim.achaibou »

thanks for you input edi
edi wrote: Sun May 21, 2023 12:26 pm

Code: Select all

    def onChanged(self,MyCube,Parameter):
        if Parameter == 'edgeLength':
            self.execute(MyCube)
I see that you call the execute function in the onChanged function which is not necessary I think. When you change the edgeLength and press Enter, the cube updates.

For now I only use the onChanged function when I have multiple objects in my FeaturePython like example below.
If you have other use cases, please give me some extra feedback.

Code: Select all

class TubeDW(tube.Tube):

    def __init__(self, obj_compound, obj_inner, obj_outer, obj_isolatie):
        self.Type = "TubeDW"
        self.Name = obj_compound.Name
        obj_compound.Proxy = self
        # add properties to the featurepython object
        obj_compound.addExtension('Part::AttachExtensionPython')
        obj_compound.addProperty('App::PropertyLinkList', 'Links')
        obj_compound.Links = [obj_inner, obj_outer, obj_isolatie]
        ...
        
    def execute(self, obj):
        obj.positionBySupport()
        obj.Shape = Part.Compound([o.Shape for o in obj.Links])
        ...
        
    def onChanged(self, obj, prop):
        if prop == "Shape" and (obj.DiaBinnenbuis != obj.Links[0].Diameter or obj.DiaBuitenbuis != obj.Links[1].Diameter):
            obj.DiaBinnenbuis = obj.Links[0].Diameter
            obj.DiaBuitenbuis = obj.Links[1].Diameter
            cutCylinder = Part.makeCylinder(obj.Links[1].Diameter, 50)
            obj.Links[2].Shape = obj.Links[1].IsolatieShapeInside.cut(obj.Links[0].IsolatieShapeOutside).cut(cutCylinder)

        if prop == "Shape" and (obj.Hoogtebuis != obj.Links[0].Hoogtebuis or obj.Hoogtebuis != obj.Links[1].Hoogtebuis):
            obj.Hoogtebuis = max(obj.Links[0].Hoogtebuis, obj.Links[1].Hoogtebuis)
            cutCylinder = Part.makeCylinder(obj.Links[1].Diameter, 50)
            obj.Links[2].Shape = obj.Links[1].IsolatieShapeInside.cut(obj.Links[0].IsolatieShapeOutside).cut(cutCylinder)
edi
Posts: 482
Joined: Fri Jan 17, 2020 1:32 pm

Re: feature python obj: init() - execute() - onChanged()

Post by edi »

karim.achaibou wrote: Mon May 22, 2023 6:04 pm I see that you call the execute function in the onChanged function which is not necessary I think.
Test 1: Comment the whole function onChanged, and run the macro. Everything works perfect, the cube changes if you change the edgeLength property.

Test 2: Uncomment the function again, and include a new first line:

Code: Select all

print('onChanged called')
Now:
- start the macro: onChanged is called 4 times.
- move or rotate the cube using: edit - transform: onChanged is called.

Conclusion: the user has no control how FreeCAD uses the callback mechanism. But it is a firm basis to create an onChanged function as used in the example. Most commands do so in C++.
Post Reply