Line drawing function

From FreeCAD Documentation
Revision as of 21:29, 4 November 2014 by Renatorivo (talk | contribs) (Created page with "Aquí se utiliza la función line() del Módulo de Pieza que crea una línea a partir de dos vectores de FreeCAD. Todo lo que creamos y modificamos dentro d...")

Esta página muestra cómo se puede crear funcionalidades avanzadas con Python. En este ejercicio, construiremos una nueva herramienta que dibuja una línea. Esta herramienta puede ser vinculada a un comando de FreeCAD, y ese comando se puede llamar desde cualquier elemento de la interfaz, tal como un elemento de menú o un botón de una barra de herramientas.

El archivo de guión principal

En primer lugar vamos a escribir un archivo de guión que contenga toda nuestra funcionalidad. Después, vamos a guardar esto en un archivo, e importarlo en FreeCAD, así todas las clases y funciones que escribas estarán disponibles para FreeCAD. De modo que inicia tu editor de texto favorito y escribe las siguientes líneas:

 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)

Explicación detallada

 import Part, FreeCADGui
 from pivy.coin import *

En Python, cuando desees utilizar las funciones de otro módulo, tienes que importarlo. En nuestro caso, vamos a necesitar las funciones del Módulo de Pieza, para la creación de la línea, y del módulo GUI (FreeCADGui), para acceder a la vista 3D. También necesitamos el contenido completo de la biblioteca de Coin, para que podamos utilizar directamente todos los objetos Coin, como SoMouseButtonEvent, etc ..

 class line:

Aquí definimos nuestra clase principal. ¿Por qué utilizar una clase y no una función? La razón es que necesitamos que nuestra herramienta se mantenga "viva" mientras esperamos a que el usuario haga clic en la pantalla. Una función termina cuando su tarea se ha hecho, pero un objeto (una clase se define como un objeto) se mantiene vivo hasta que se destruye.

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

En Python, cada clase o función puede tener una cadena de descripción. Esto es particularmente útil en FreeCAD, porque cuando vas a llamar a esa clase en el intérprete, la cadena de descripción se mostrará como una nota.

 def __init__(self):

Las clases en Python siempre pueden contener una función __init__, que se ejecuta cuando la clase es llamada para crear un objeto. Por lo tanto, vamos a poner aquí todo lo que queremos que ocurra cuando nuestra herramienta línea comienza.

 self.view = FreeCADGui.ActiveDocument.ActiveView

En una clase, por lo general querrás incluir self. antes de un nombre de variable, para que sea fácilmente accesible por todas las funciones dentro y fuera de esa clase. Aquí, vamos a utilizar self.view para acceder y manipular la vista 3D activa.

 self.stack = []

Aquí creamos una lista vacía que contendrá los puntos 3D enviados por la función getpoint.

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

Aquí viene la parte importante: Dado que en realidad se trata de una escena Coin3d, FreeCAD utiliza el mecanismo de devolución de llamada de Coin, que permite que una función sea llamada cada vez que sucede un determinado evento de escena. En nuestro caso, estamos creando una devolución de llamada para eventos SoMouseButtonEvent, y lo conduciremos a la función getpoint. Ahora, cada vez que un botón del ratón sea pulsado o soltado, la función getpoint será ejecutada.

Ten en cuenta que también hay una alternativa a addEventCallbackPivy(), llamada addEventCallback(), que dispensa del uso de pivy. Pero como pivy es una forma muy eficaz y natural para acceder a cualquier parte de la escena de Coin, es mucho mejor usarlo tanto como se pueda!

 def getpoint(self,event_cb):

Ahora definimos la función getpoint, que se ejecutará cuando el botón del ratón se pulsa en una vista 3D. Esta función recibe un argumento, que llamaremos event_cb. A partir de este evento de devolución de llamada podemos tener acceso al objeto de evento, que contiene varias piezas de información (más información aquí).

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

La función getpoint se llamará cuando un botón del ratón sea pulsado o soltado. Pero queremos escoger un punto 3D sólo cuando se presiona (de lo contrario obtendríamos dos puntos 3D muy cerca uno del otro). Por lo tanto, debes comprobar eso aquí.

 pos = event.getPosition()

Aquí obtenemos las coordenadas de pantalla del cursor del ratón

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

Esta función nos da un vector de FreeCAD (x,y,z) que contiene el punto 3D que se encuentra en el plano focal, justo debajo del cursor de nuestro ratón. Si estás en vista de cámara, imagina un rayo proveniente de la cámara, pasando por el cursor del ratón, y alcanzando el plano focal. Ahí está nuestro punto 3D. Si estamos en una vista ortogonal, el rayo es paralelo a la dirección de la vista.

 self.stack.append(point)

Añadimos nuestro nuevo punto a la pila

 if len(self.stack) == 2:

¿Tenemos ya suficientes puntos? si es así, entonces vamos a trazar la línea!

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

Aquí se utiliza la función line() del Módulo de Pieza que crea una línea a partir de dos vectores de FreeCAD. Todo lo que creamos y modificamos dentro del módulo de Pieza, se queda en el módulo de Pieza . Así, hasta ahora, hemos creado un elemento de línea. No está ligado a un objeto de nuestro documento activo, por lo que no aparece nada en la pantalla.

 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