Line drawing function/fr: Difference between revisions

From FreeCAD Documentation
(Updating to match new version of source page)
Line 1: Line 1:
This page shows how advanced functionality can easily be built in Python. In this exercise, we will be building a new tool that draws a line. This tool can then be linked to a FreeCAD command, and that command can be called by any element of the interface, like a menu item or a toolbar button.
= Line drawing function/fr =


==The main script==
__TOC__
First we will write a script containing all our functionality. Then, we will save this in a file, and import it in FreeCAD, so all classes and functions we write will be availible to FreeCAD. So, launch your favorite text editor, and type the following lines:

Cette page montre comment construire facilement des fonctionnalités avancées en Python. Dans cet exercice, nous allons construire un nouvel outil qui trace une ligne. Cet outil peut alors être lié à une commande FreeCAD, et cette commande peut être appelée par n'importe quel élément de l'interface, comme un élément de menu ou un bouton de la barre d'outils.

==Script principal==

Première chose, nous allons écrire un script contenant toutes nos fonctionnalités, puis, nous allons l'enregistrer dans un fichier, et l'importer dans FreeCAD, alors toutes les classes et fonctions que nous écrirons seront accessibles à partir de FreeCAD.<br>
Alors, lancez votre éditeur de texte favori, et entrez les lignes suivantes:
<syntaxhighlight>
<syntaxhighlight>
import FreeCADGui, Part
import FreeCADGui, Part
Line 32: Line 26:
self.view.removeEventCallbackPivy(SoMouseButtonEvent.getClassTypeId(),self.callback)
self.view.removeEventCallbackPivy(SoMouseButtonEvent.getClassTypeId(),self.callback)
</syntaxhighlight>
</syntaxhighlight>
==Detailed explanation==

<syntaxhighlight>
==Explications détaillées==

import Part, FreeCADGui
import Part, FreeCADGui
from pivy.coin import *
from pivy.coin import *
</syntaxhighlight>

In Python, when you want to use functions from another module, you need to import it. In our case, we will need functions from the [[Part Module]], for creating the line, and from the Gui module (FreeCADGui), for accessing the 3D view. We also need the complete contents of the coin library, so we can use directly all coin objects like SoMouseButtonEvent, etc...
En Python, lorsque vous voulez utiliser les fonctions d'un autre module, vous avez besoin de l'importer.<br>
<syntaxhighlight>
Dans notre cas, nous aurons besoin de fonctions du [[Part Module/fr|'''Part Module''']], pour la création de la ligne, et du '''Gui module''' (FreeCADGui), pour accéder à la vue 3D.<br>
Nous avons également besoin de tout le contenu de la bibliothèque de pièces, afin que nous puissions utiliser directement tous les objets comme '''coin''', '''SoMouseButtonEvent''' (évènement souris) etc ..

class line:
class line:
</syntaxhighlight>

Here we define our main class. Why do we use a class and not a function? The reason is that we need our tool to stay "alive" while we are waiting for the user to click on the screen. A function ends when its task has been done, but an object (a class defines an object) stays alive until it is destroyed.
Ici, nous définissons notre classe principale.<br>
<syntaxhighlight>
Mais pourquoi utilisons-nous une classe et non une fonction ? La raison en est que nous avons besoin que notre outil reste "vivant" en attendant que l'utilisateur clique sur l'écran.<br>
* Une fonction se termine lorsque sa tâche est terminée,
* mais un objet, '''(une classe définit un objet)''' reste en vie (actif) jusqu'à ce qu'il soit détruit.

"this class will create a line after the user clicked 2 points on the screen"
"this class will create a line after the user clicked 2 points on the screen"
</syntaxhighlight>

In Python, every class or function can have a description string. This is particularly useful in FreeCAD, because when you'll call that class in the interpreter, the description string will be displayed as a tooltip.
En Python, toutes les classes ou fonctions peuvent avoir une description.<br>
<syntaxhighlight>
Ceci est particulièrement utile dans FreeCAD, parce que quand vous appelez cette classe dans l'interpréteur, la description sera affichée comme une '''info-bulle'''.

def __init__(self):
def __init__(self):
</syntaxhighlight>

Python classes can always contain an __init__ function, which is executed when the class is called to create an object. So, we will put here everything we want to happen when our line tool begins.
Les classes Python doivent toujours contenir une fonction '''__ init__''', qui est exécutée lorsque la classe est appelée pour créer un objet.<br>
<syntaxhighlight>
Donc, nous allons mettre ici tout ce que nous voulons produire lorsque notre outil de création de ligne commence (appelé).

self.view = FreeCADGui.ActiveDocument.ActiveView
self.view = FreeCADGui.ActiveDocument.ActiveView
</syntaxhighlight>

In a class, you usually want to append ''self.'' before a variable name, so it will be easily accessible to all functions inside and outside that class. Here, we will use self.view to access and manipulate the active 3D view.
Dans une classe, il est généralement souhaitable d'ajouter '''self.''' devant un nom de variable, de sorte que la variable sera facilement accessible à toutes les fonctions à l'intérieur et à l'extérieur de cette classe.<br>
<syntaxhighlight>
Ici, nous allons utiliser '''self.view''' pour accéder et manipuler la vue active 3D.

self.stack = []
self.stack = []
</syntaxhighlight>

Here we create an empty list that will contain the 3D points sent by the getpoint function.
Ici, nous créons une liste vide qui contiendra les '''points''' en 3D envoyés par la fonction '''GetPoint'''.
<syntaxhighlight>

self.callback = self.view.addEventCallbackPivy(SoMouseButtonEvent.getClassTypeId(),self.getpoint)
self.callback = self.view.addEventCallbackPivy(SoMouseButtonEvent.getClassTypeId(),self.getpoint)
</syntaxhighlight>
This is the important part: Since it is actually a [http://www.coin3d.org/ coin3D] scene, the FreeCAD uses coin callback mechanism, that allows a function to be called everytime a certain scene event happens. In our case, we are creating a callback for [http://doc.coin3d.org/Coin/group__events.html SoMouseButtonEvent] events, and we bind it to the getpoint function. Now, everytime a mouse button is pressed or released, the getpoint function will be executed.


Note that there is also an alternative to addEventCallbackPivy() called addEventCallback() which dispenses the use of pivy. But since pivy is a very efficient and natural way to access any part of the coin scene, it is much better to use it as much as you can!
'''Ceci est un point important:'''<br>
<syntaxhighlight>
Du fait qu'il s'agit d'une scène [http://www.coin3d.org/ coin3D], FreeCAD utilise les mécanismes de rappel de '''coin''', qui permet à une fonction d'être appelée à chaque fois qu'un évènement se passe sur la scène.<br>
Dans notre cas, nous créons un appel pour [http://doc.coin3d.org/Coin/group__events.html SoMouseButtonEvent], et nous le lions à la fonction '''GetPoint'''.<br>
Maintenant, chaque fois qu'un bouton de la souris est enfoncé ou relâché, la fonction '''GetPoint''' sera exécutée.<br>
Notez qu'il existe aussi une alternative à '''addEventCallbackPivy()''' appelée '''addEventCallback()''' qui dispense l'utilisation de '''pivy'''. Mais, '''pivy''' est un moyen très simple et efficace d'accéder à n'importe quelle partie de la scène '''coin''', il est conseillé de l'utiliser autant que possible !

def getpoint(self,event_cb):
def getpoint(self,event_cb):
</syntaxhighlight>

Now we define the getpoint function, that will be executed when a mouse button is pressed in a 3D view. This function will receive an argument, that we will call event_cb. From this event callback we can access the event object, which contains several pieces of information (mode info [[Code_snippets#Observing_mouse_events_in_the_3D_viewer_via_Python|here]]).
Maintenant, nous définissons la fonction '''GetPoint''', qui sera exécutée quand un bouton de la souris sera pressé dans une vue 3D.<br>
<syntaxhighlight>
Cette fonction recevra un argument, que nous appellerons '''event_cb'''. A partir de l'appel de cet événement, nous pouvons accéder à l'objet événement, qui contient plusieurs éléments d'information ([http://www.freecadweb.org/wiki/index.php?title=Code_snippets/fr#Observation_des_.C3.A9v.C3.A8nements_de_la_souris_dans_la_vue_3D_via_Python plus d'informations sur cette page]).

if event.getState() == SoMouseButtonEvent.DOWN:
if event.getState() == SoMouseButtonEvent.DOWN:
</syntaxhighlight>

The getpoint function will be called when a mouse button is pressed or released. But we want to pick a 3D point only when pressed (otherwise we would get two 3D points very close to each other). So we must check for that here.
La fonction '''GetPoint''' sera appelée dès qu'un bouton de la souris est enfoncé ou relâché. Mais, nous ne voulons prendre un point 3D uniquement lorsqu'il est pressé (sinon, nous aurons deux points 3D très proches l'un de l'autre).<br>
<syntaxhighlight>
Donc, nous devons vérifier cela avec:

pos = event.getPosition()
pos = event.getPosition()
</syntaxhighlight>

Here we get the screen coordinates of the mouse cursor
Ici, nous avons les coordonnées du curseur de la souris sur l'écran
<syntaxhighlight>

point = self.view.getPoint(pos[0],pos[1])
point = self.view.getPoint(pos[0],pos[1])
</syntaxhighlight>

This function gives us a FreeCAD vector (x,y,z) containing the 3D point that lies on the focal plane, just under our mouse cursor. If you are in camera view, imagine a ray coming from the camera, passing through the mouse cursor, and hitting the focal plane. There is our 3D point. If we are in orthogonal view, the ray is parallel to the view direction.
Cette fonction nous donne le vecteur ('''x, y, z''') du point qui se trouve sur le plan focal, juste sous curseur de notre souris.<br>
<syntaxhighlight>
Si vous êtes dans la vue caméra, imaginez un rayon provenant de la caméra, en passant par le curseur de la souris, et en appuyant sur le plan focal.<br>
C'est notre point dans la vue 3D. Si l'on est en mode orthogonal, le rayon est parallèle à la direction de la vue.

self.stack.append(point)
self.stack.append(point)
</syntaxhighlight>

Nous ajoutons notre nouveau point sur la pile
We add our new point to the stack
<syntaxhighlight>

if len(self.stack) == 2:
if len(self.stack) == 2:
</syntaxhighlight>

Avons nous tous les points ? si oui, alors nous allons tracer la ligne !
Do we have enough points already? if yes, then let's draw the line!
<syntaxhighlight>

l = Part.Line(self.stack[0],self.stack[1])
l = Part.Line(self.stack[0],self.stack[1])
</syntaxhighlight>

Here we use the function Line() from the [[Part Module]] that creates a line from two FreeCAD vectors. Everything we create and modify inside the Part module, stays in the Part module. So, until now, we created a Line Part. It is not bound to any object of our active document, so nothing appears on the screen.
Ici, nous utilisons la fonction '''line()''' de [[Part Module/fr|Part Module]] qui crée une ligne de deux vecteurs FreeCAD.<br>
<syntaxhighlight>
Tout ce que nous créons et modifions l'intérieur de '''Part Module''', reste dans le '''Part Module'''.<br>
Donc, jusqu'à présent, nous avons créé une '''Line Part'''. Il n'est lié à aucun objet de notre document actif, c'est pour cela que rien ne s'affiche sur l'écran.

shape = l.toShape()
shape = l.toShape()
</syntaxhighlight>

The FreeCAD document can only accept shapes from the Part module. Shapes are the most generic type of the Part module. So, we must convert our line to a shape before adding it to the document.
Le document FreeCAD ne peut accepter que des formes à partir de Part Module. Les formes sont le type le plus courant de Part Module.<br>
<syntaxhighlight>
Donc, nous devons transformer notre ligne en une forme avant de l'ajouter au document.

Part.show(shape)
Part.show(shape)
</syntaxhighlight>

The Part module has a very handy show() function that creates a new object in the document and binds a shape to it. We could also have created a new object in the document first, then bound the shape to it manually.
Le Part module a une fonction très pratique '''show()''' qui crée un nouvel objet dans le document et se lie a une forme.<br>
<syntaxhighlight>
Nous aurions aussi pu créer un nouvel objet dans le premier document, puis le lier à la forme manuellement.

self.view.removeEventCallbackPivy(SoMouseButtonEvent.getClassTypeId(),self.callback)
self.view.removeEventCallbackPivy(SoMouseButtonEvent.getClassTypeId(),self.callback)
</syntaxhighlight>
Since we are done with our line, let's remove the callback mechanism, that consumes precious CPU cycles.


==Testing & Using the script==
Maintenant, nous en avons fini avec notre ligne, nous allons supprimer le mécanisme de rappel, qui consomme de précieux cycles de CPU.
Now, let's save our script to some place where the FreeCAD python interpreter will find it. When importing modules, the interpreter will look in the following places: the python installation paths, the FreeCAD bin directory, and all FreeCAD modules directories. So, the best solution is to create a new directory in one of the FreeCAD [[Installing_more_workbenches|Mod directories]], and to save our script in it. For example, let's make a "MyScripts" directory, and save our script as "exercise.py".

==Tester et utiliser un script==

Maintenant, nous allons enregistrer notre script dans un endroit où l'interpréteur Python de FreeCAD le trouvera.<br>
Lors de l'importation de modules, l’interpréteur cherchera dans les endroits suivants:<br>
* les chemins d'installation de python,
* le répertoire bin FreeCAD,
* et tous les répertoires des modules FreeCAD.<br>
Donc, la meilleure solution est de créer un nouveau répertoire dans le répertoire Mod de FreeCAD , et sauver votre script dans ce répertoire.<br>
Par exemple, nous allons créer un répertoire '''"myscripts"''', et sauver notre script comme '''"exercise.py"'''.<br><br>
Maintenant, tout est prêt, nous allons commencer par créez un nouveau document FreeCAD, et, dans l'interpréteur Python, tapons:


Now, everything is ready, let's start FreeCAD, create a new document, and, in the python interpreter, issue:
<syntaxhighlight>
import exercise
import exercise
</syntaxhighlight>

If no error message appear, that means our exercise script has been loaded. We can now check its contents with:
Si aucun message d'erreur n'apparaît, cela signifie que notre script '''exercise''' a été chargé.<br>
<syntaxhighlight>
Nous pouvons maintenant lister son contenu avec:

dir(exercise)
dir(exercise)
</syntaxhighlight>

La commande '''dir()''' est une commande intégrée dans python, et lister le contenu d'un module. Nous pouvons voir que notre '''classe line()''' est qui nous attend.<br> Maintenant, nous allons le tester:
The command dir() is a built-in python command that lists the contents of a module. We can see that our line() class is there, waiting for us. Now let's test it:
<syntaxhighlight>

exercise.line()
exercise.line()
</syntaxhighlight>
Then, click two times in the 3D view, and bingo, here is our line! To do it again, just type exercise.line() again, and again, and again... Feels great, no?


==Registering the script in the FreeCAD interface==
Puis, cliquez deux fois dans la vue 3D, et bingo, voici notre ligne ! Pour la faire de nouveau, tapez juste exercise.line(), encore et encore, et encore ... C'est bien, non?
Now, for our new line tool to be really cool, it should have a button on the interface, so we don't need to type all that stuff everytime. The easiest way is to transform our new MyScripts directory into a full FreeCAD workbench. It is easy, all that is needed is to put a file called '''InitGui.py''' inside your MyScripts directory. The InitGui.py will contain the instructions to create a new workbench, and add our new tool to it. Besides that we will also need to transform a bit our exercise code, so the line() tool is recognized as an official FreeCAD command. Let's start by making an InitGui.py file, and write the following code in it:

==Enregistrement du script dans l'interface de FreeCAD==

Maintenant, pour que notre outil de création de ligne soit vraiment cool, il devrait y avoir un bouton sur l'interface, nous n'aurons donc pas besoin de taper tout ce code à chaque fois.<br>
Le plus simple est de transformer notre nouveau répertoire '''myscripts''' dans un plan de travail FreeCAD. C'est facile, tout ce qui est nécessaire de faire, est de mettre un fichier appelé '''InitGui.py''' à l'intérieur de votre répertoire '''myscripts'''.<br>
Le fichier '''InitGui.py''' contiendra les instructions pour créer un nouveau plan de travail, et s'ajoutera notre nouvel outil.<br>
Sans oublier, que nous aurons aussi besoin de transformer un peu notre code '''exercise''', de sorte que l'outil '''line()''' soit reconnu comme une commande FreeCAD officielle.<br>
Commençons par faire un fichier '''InitGui.py''', et écrivons le code suivant à l'intérieur:
<syntaxhighlight>
<syntaxhighlight>
class MyWorkbench (Workbench):
class MyWorkbench (Workbench):
Line 164: Line 127:
Gui.addWorkbench(MyWorkbench())
Gui.addWorkbench(MyWorkbench())
</syntaxhighlight>
</syntaxhighlight>
By now, you should already understand the above script by yourself, I think: We create a new class that we call MyWorkbench, we give it a title (MenuText), and we define an Initialize() function that will be executed when the workbench is loaded into FreeCAD. In that function, we load in the contents of our exercise file, and append the FreeCAD commands found inside to a command list. Then, we make a toolbar called "My Scripts" and we assign our commands list to it. Currently, of course, we have only one tool, so our command list contains only one element. Then, once our workbench is ready, we add it to the main interface.
Actuellement, vous devriez comprendre le script ci-dessus par vous-même, du moins, je pense:<br>

Nous créons une nouvelle classe que nous appelons '''MyWorkbench''', nous lui donnons un nom (MenuText), et nous définissons une fonction '''Initialize()''' qui sera exécutée quand le plan de travail sera chargé dans FreeCAD.<br>
But this still won't work, because a FreeCAD command must be formatted in a certain way to work. So we will need to transform a bit our line() tool. Our new exercise.py script will now look like this:
Dans cette fonction, nous chargeons le contenus de notre fichier ''''exercise''', et ajoutons les commandes FreeCAD trouvées dans une liste de commandes. Ensuite, nous faisons une barre d'outils appelée "'''Mes scripts'''" et nous attribuons notre liste des commandes.<br><br>
Actuellement, bien sûr, nous n'avons qu'un seul outil, puisque notre liste de commandes ne contient qu'un seul élément. Puis, une fois que notre plan de travail est prêt, nous l'ajoutons à l'interface principale.<br>
Mais, cela ne fonctionne toujours pas, car une commande FreeCAD doit être formatée d'une certaine façon pour travailler. Nous aurons donc besoin de transformer un peu notre outil '''ligne()'''.<br>
Notre nouveau script '''exercise.py''' va maintenant ressembler à ceci:
<syntaxhighlight>
<syntaxhighlight>
import FreeCADGui, Part
import FreeCADGui, Part
Line 194: Line 154:
FreeCADGui.addCommand('line', line())
FreeCADGui.addCommand('line', line())
</syntaxhighlight>
</syntaxhighlight>
What we did here is transform our __init__() function into an Activated() function, because when FreeCAD commands are run, they automatically execute the Activated() function. We also added a GetResources() function, that informs FreeCAD where it can find an icon for the tool, and what will be the name and tooltip of our tool. Any jpg, png or svg image will work as an icon, it can be any size, but it is best to use a size that is close to the final aspect, like 16x16, 24x24 or 32x32.
Qu'avons fait ici ? nous avons transformé notre fonction ''' __ init__ ()''' en une fonction '''Activated()''', parce que lorsque les commandes sont exécutées dans FreeCAD, il exécute automatiquement la fonction '''Activated()'''.<br>
Then, we add the line() class as an official FreeCAD command with the addCommand() method.
Nous avons également ajouté une fonction '''GetResources()''', qui informe FreeCAD où se trouve l'icône de l'outil, le nom et l'info-bulle de l'outil.<br>

Toute image, jpg, png ou svg peut être utilisé comme icône, il peut être de n'importe quelle taille, mais il est préférable d'utiliser une taille standard qui est proche de l'aspect final, comme 16x16, 24x24 ou 32x32.<br>
That's it, we now just need to restart FreeCAD and we'll have a nice new workbench with our brand new line tool!
Puis, nous ajoutons notre '''class line()''' comme une commande officielle de FreeCAD avec la méthode '''addCommand()'''.<br><br>

Ça y est, nous avons juste besoin de redémarrer FreeCAD et nous aurons un plan de travail agréable avec notre nouvel outil '''ligne''' tout neuf !
==So you want more?==


If you liked this exercise, why not try to improve this little tool? There are many things that can be done, like for example:
==Vous voulez en savoir plus ?==
* Add user feedback: until now we did a very bare tool, the user might be a bit lost when using it. So we could add some feedback, telling him what to do next. For example, you could issue messages to the FreeCAD console. Have a look in the FreeCAD.Console module
Si vous avez aimé cet '''"exercise"''', pourquoi ne pas essayer d'améliorer ce petit outil ? Il y a beaucoup de choses à faire, comme par exemple:<br>
* Add a possibility to type the 3D points coordinates manually. Look at the python input() function, for example
* Ajouter des Commentaires utilisateur: jusqu'à présent nous avons fait un outil très dépouillé, l'utilisateur peut être un peu perdu lors de son utilisation. Vous pouvez ajouter vos commentaires, en guidant l'utilisateur. Par exemple, vous pourriez émettre des messages à la console FreeCAD. "Jetez" un oeil dans le module '''FreeCAD.Console'''
* Add the possibility to add more than 2 points
* Ajouter la possibilité d'entrer les coordonnées 3D manuellement . Regardez les fonctions Python input(), par exemple
* Add events for other things: Now we just check for Mouse button events, what if we would also do something when the mouse is moved, like displaying current coordinates?
* Ajouter la possibilité d'ajouter plus de 2 points
* Give a name to the created object
* Ajouter des événements pour d'autres fonctions: Maintenant que nous venons d'apprendre les événements de bouton de souris, si nous souhaitons également faire quelque chose quand la souris est déplacée, comme par exemple l'affichage des coordonnées actuelles?
Don't hesitate to write your questions or ideas on the [[Talk:Line_drawing_function|talk page]]!
* Donnez un nom à l'objet créé et bien d'autres choses<br><br>
N'hésitez pas de commenter vos idées ou questions sur le [http://forum.freecadweb.org/ forum] !


{{docnav/fr|[[Code snippets/fr|Code snippets]]|[[Dialog creation/fr|Dialog creation]]}}
{{docnav|Code snippets|Dialog creation}}


[[Category:Poweruser Documentation]]
{{languages/fr | {{en|Line drawing function}} {{es|Line drawing function/es}} {{it|Line drawing function/it}} {{ru|Line drawing function/ru}} {{se|Line drawing function/se}} }}
[[Category:Python Code]]


{{clear}}
[[Category:Poweruser Documentation/fr]]
<languages/>
[[Category:Python Code/fr]]

Revision as of 19:47, 4 November 2014

This page shows how advanced functionality can easily be built in Python. In this exercise, we will be building a new tool that draws a line. This tool can then be linked to a FreeCAD command, and that command can be called by any element of the interface, like a menu item or a toolbar button.

The main script

First we will write a script containing all our functionality. Then, we will save this in a file, and import it in FreeCAD, so all classes and functions we write will be availible to FreeCAD. So, launch your favorite text editor, and type the following lines:

 import FreeCADGui, Part
 from pivy.coin import *
 
 class line:
     "this class will create a line after the user clicked 2 points on the screen"
     def __init__(self):
         self.view = FreeCADGui.ActiveDocument.ActiveView
         self.stack = []
         self.callback = self.view.addEventCallbackPivy(SoMouseButtonEvent.getClassTypeId(),self.getpoint)  
 
     def getpoint(self,event_cb):
         event = event_cb.getEvent()
         if event.getState() == SoMouseButtonEvent.DOWN:
             pos = event.getPosition()
             point = self.view.getPoint(pos[0],pos[1])
             self.stack.append(point)
             if len(self.stack) == 2:
                 l = Part.Line(self.stack[0],self.stack[1])
                 shape = l.toShape()
                 Part.show(shape)
                 self.view.removeEventCallbackPivy(SoMouseButtonEvent.getClassTypeId(),self.callback)

Detailed explanation

 import Part, FreeCADGui
 from pivy.coin import *

In Python, when you want to use functions from another module, you need to import it. In our case, we will need functions from the Part Module, for creating the line, and from the Gui module (FreeCADGui), for accessing the 3D view. We also need the complete contents of the coin library, so we can use directly all coin objects like SoMouseButtonEvent, etc...

 class line:

Here we define our main class. Why do we use a class and not a function? The reason is that we need our tool to stay "alive" while we are waiting for the user to click on the screen. A function ends when its task has been done, but an object (a class defines an object) stays alive until it is destroyed.

 "this class will create a line after the user clicked 2 points on the screen"

In Python, every class or function can have a description string. This is particularly useful in FreeCAD, because when you'll call that class in the interpreter, the description string will be displayed as a tooltip.

 def __init__(self):

Python classes can always contain an __init__ function, which is executed when the class is called to create an object. So, we will put here everything we want to happen when our line tool begins.

 self.view = FreeCADGui.ActiveDocument.ActiveView

In a class, you usually want to append self. before a variable name, so it will be easily accessible to all functions inside and outside that class. Here, we will use self.view to access and manipulate the active 3D view.

 self.stack = []

Here we create an empty list that will contain the 3D points sent by the getpoint function.

 self.callback = self.view.addEventCallbackPivy(SoMouseButtonEvent.getClassTypeId(),self.getpoint)

This is the important part: Since it is actually a coin3D scene, the FreeCAD uses coin callback mechanism, that allows a function to be called everytime a certain scene event happens. In our case, we are creating a callback for SoMouseButtonEvent events, and we bind it to the getpoint function. Now, everytime a mouse button is pressed or released, the getpoint function will be executed.

Note that there is also an alternative to addEventCallbackPivy() called addEventCallback() which dispenses the use of pivy. But since pivy is a very efficient and natural way to access any part of the coin scene, it is much better to use it as much as you can!

 def getpoint(self,event_cb):

Now we define the getpoint function, that will be executed when a mouse button is pressed in a 3D view. This function will receive an argument, that we will call event_cb. From this event callback we can access the event object, which contains several pieces of information (mode info here).

 if event.getState() == SoMouseButtonEvent.DOWN:

The getpoint function will be called when a mouse button is pressed or released. But we want to pick a 3D point only when pressed (otherwise we would get two 3D points very close to each other). So we must check for that here.

 pos = event.getPosition()

Here we get the screen coordinates of the mouse cursor

 point = self.view.getPoint(pos[0],pos[1])

This function gives us a FreeCAD vector (x,y,z) containing the 3D point that lies on the focal plane, just under our mouse cursor. If you are in camera view, imagine a ray coming from the camera, passing through the mouse cursor, and hitting the focal plane. There is our 3D point. If we are in orthogonal view, the ray is parallel to the view direction.

 self.stack.append(point)

We add our new point to the stack

 if len(self.stack) == 2:

Do we have enough points already? if yes, then let's draw the line!

 l = Part.Line(self.stack[0],self.stack[1])

Here we use the function Line() from the Part Module that creates a line from two FreeCAD vectors. Everything we create and modify inside the Part module, stays in the Part module. So, until now, we created a Line Part. It is not bound to any object of our active document, so nothing appears on the screen.

 shape = l.toShape()

The FreeCAD document can only accept shapes from the Part module. Shapes are the most generic type of the Part module. So, we must convert our line to a shape before adding it to the document.

 Part.show(shape)

The Part module has a very handy show() function that creates a new object in the document and binds a shape to it. We could also have created a new object in the document first, then bound the shape to it manually.

 self.view.removeEventCallbackPivy(SoMouseButtonEvent.getClassTypeId(),self.callback)

Since we are done with our line, let's remove the callback mechanism, that consumes precious CPU cycles.

Testing & Using the script

Now, let's save our script to some place where the FreeCAD python interpreter will find it. When importing modules, the interpreter will look in the following places: the python installation paths, the FreeCAD bin directory, and all FreeCAD modules directories. So, the best solution is to create a new directory in one of the FreeCAD Mod directories, and to save our script in it. For example, let's make a "MyScripts" directory, and save our script as "exercise.py".

Now, everything is ready, let's start FreeCAD, create a new document, and, in the python interpreter, issue:

 import exercise

If no error message appear, that means our exercise script has been loaded. We can now check its contents with:

 dir(exercise)

The command dir() is a built-in python command that lists the contents of a module. We can see that our line() class is there, waiting for us. Now let's test it:

 exercise.line()

Then, click two times in the 3D view, and bingo, here is our line! To do it again, just type exercise.line() again, and again, and again... Feels great, no?

Registering the script in the FreeCAD interface

Now, for our new line tool to be really cool, it should have a button on the interface, so we don't need to type all that stuff everytime. The easiest way is to transform our new MyScripts directory into a full FreeCAD workbench. It is easy, all that is needed is to put a file called InitGui.py inside your MyScripts directory. The InitGui.py will contain the instructions to create a new workbench, and add our new tool to it. Besides that we will also need to transform a bit our exercise code, so the line() tool is recognized as an official FreeCAD command. Let's start by making an InitGui.py file, and write the following code in it:

 class MyWorkbench (Workbench): 
    MenuText = "MyScripts"
    def Initialize(self):
        import exercise
        commandslist = ["line"]
        self.appendToolbar("My Scripts",commandslist)
 Gui.addWorkbench(MyWorkbench())

By now, you should already understand the above script by yourself, I think: We create a new class that we call MyWorkbench, we give it a title (MenuText), and we define an Initialize() function that will be executed when the workbench is loaded into FreeCAD. In that function, we load in the contents of our exercise file, and append the FreeCAD commands found inside to a command list. Then, we make a toolbar called "My Scripts" and we assign our commands list to it. Currently, of course, we have only one tool, so our command list contains only one element. Then, once our workbench is ready, we add it to the main interface.

But this still won't work, because a FreeCAD command must be formatted in a certain way to work. So we will need to transform a bit our line() tool. Our new exercise.py script will now look like this:

 import FreeCADGui, Part
 from pivy.coin import *
 class line:
  "this class will create a line after the user clicked 2 points on the screen"
  def Activated(self):
    self.view = FreeCADGui.ActiveDocument.ActiveView
    self.stack = []
    self.callback = self.view.addEventCallbackPivy(SoMouseButtonEvent.getClassTypeId(),self.getpoint) 
  def getpoint(self,event_cb):
    event = event_cb.getEvent()
    if event.getState() == SoMouseButtonEvent.DOWN:
      pos = event.getPosition()
      point = self.view.getPoint(pos[0],pos[1])
      self.stack.append(point)
      if len(self.stack) == 2:
        l = Part.Line(self.stack[0],self.stack[1])
        shape = l.toShape()
        Part.show(shape)
        self.view.removeEventCallbackPivy(SoMouseButtonEvent.getClassTypeId(),self.callback)
  def GetResources(self): 
      return {'Pixmap' : 'path_to_an_icon/line_icon.png', 'MenuText': 'Line', 'ToolTip': 'Creates a line by clicking 2 points on the screen'} 
 FreeCADGui.addCommand('line', line())

What we did here is transform our __init__() function into an Activated() function, because when FreeCAD commands are run, they automatically execute the Activated() function. We also added a GetResources() function, that informs FreeCAD where it can find an icon for the tool, and what will be the name and tooltip of our tool. Any jpg, png or svg image will work as an icon, it can be any size, but it is best to use a size that is close to the final aspect, like 16x16, 24x24 or 32x32. Then, we add the line() class as an official FreeCAD command with the addCommand() method.

That's it, we now just need to restart FreeCAD and we'll have a nice new workbench with our brand new line tool!

So you want more?

If you liked this exercise, why not try to improve this little tool? There are many things that can be done, like for example:

  • Add user feedback: until now we did a very bare tool, the user might be a bit lost when using it. So we could add some feedback, telling him what to do next. For example, you could issue messages to the FreeCAD console. Have a look in the FreeCAD.Console module
  • Add a possibility to type the 3D points coordinates manually. Look at the python input() function, for example
  • Add the possibility to add more than 2 points
  • Add events for other things: Now we just check for Mouse button events, what if we would also do something when the mouse is moved, like displaying current coordinates?
  • Give a name to the created object

Don't hesitate to write your questions or ideas on the talk page!

Code snippets
Dialog creation