Scripts/es

Tutorial
Topic
Scripting
Level
Básico
Time to complete
Authors
onekk Carlo
FreeCAD version
0.19
Example files
See also
None

Introducción

Con el término "scripting" nos referimos a la creación de objetos topológicos mediante el intérprete de Python de FreeCAD. FreeCAD podría ser una excelente alternativa a OpenSCAD, principalmente porque cuenta con un intérprete de Python real, lo que significa que dispone de un lenguaje de programación integrado. Prácticamente, todo lo que se puede hacer con la interfaz gráfica de usuario (GUI) se puede realizar mediante un script de Python.

Lamentablemente, la información sobre la programación de scripts en la documentación, incluso en esta wiki, está dispersa y carece de la necesaria uniformidad en la forma de escribirla; además, la mayoría de las explicaciones se realizan de una manera demasiado técnica.

Primeros pasos

El primer obstáculo para programar de una manera sencilla los scripts es que no hay una forma directa de acceder al editor interno de Python de FreeCAD a través de un elemento de menú o un icono en el área de la barra de herramientas, pero sabiendo que FreeCAD abre un archivo con una extensión .py en el editor interno de Python, el truco más sencillo es crearlo directamente en su editor de texto favorito y luego abrirlo con el comando habitual Archivo → Abrir.

Para hacer las cosas de una manera estandarizada, el archivo debe escribirse con cierto orden. El editor de Python de FreeCAD tiene un buen "resaltado de sintaxis" del que carecen muchos editores sencillos como el Bloc de notas de Windows o algunos editores básicos de Linux, por lo que basta con escribir estas pocas líneas:

"""filename.py

   A short description of what the script does

"""

Guárdelos con un nombre significativo con la extensión .py y cargue el archivo resultante en FreeCAD, con el comando Archivo → Abrir.

En este fragmento de código se muestra el mínimo ejemplo de lo que es necesario incluir en un script que podría utilizarse como plantilla para casi cualquier script futuro:

"""filename.py

   First FreeCAD Script

"""

import FreeCAD
from FreeCAD import Placement, Rotation, Vector
import FreeCADGui

DOC_NAME = "Wiki_Example"
DOC = FreeCAD.newDocument(DOC_NAME)
FreeCAD.setActiveDocument(DOC.Name)

ROT0 = Rotation(0, 0, 0)
VEC0 = Vector(0, 0, 0)

# Helper function

def set_view():
    """Rearrange View."""
    if not FreeCAD.GuiUp:
        return
    doc = FreeCADGui.ActiveDocument
    if doc is None:
        return
    view = doc.ActiveView
    if view is None:
        return
    # Check if the view is a 3D view:
    if not hasattr(view, "getSceneGraph"):
        return
    view.viewAxometric()
    view.fitAll()

En el código anterior se incorporan algunos trucos:

Comencemos con un pequeño script que realiza una tarea muy sencilla, pero que demuestra el poder de este enfoque.

# Script functions

def my_box(name, len, wid, hei):
    """Create a box."""
    obj_b = DOC.addObject("Part::Box", name)
    obj_b.Length = len
    obj_b.Width = wid
    obj_b.Height = hei

    DOC.recompute()

    return obj_b

# objects definition

obj = my_box("test_cube", 5, 5, 5)

set_view()

Escriba las líneas de código anteriores después de # Script functions y presione la flecha verde en la Barra de herramientas de macros.

Verá que suceden algunas cosas sorprendentes. Se abrirá un nuevo documento llamado "Wiki_example" y en la vista 3D aparecerá un Cubo, como el de la imagen de abajo.

Test cube

Algo más

¿No es asombroso? Sí, pero tenemos que empezar por algún lado. Podemos hacer lo mismo con un Cilindro, agregando estas líneas de código después de la función my_box() y antes de la línea: # objects definition.

def my_cyl(name, ang, rad, hei):
    """Create a Cylinder."""
    obj = DOC.addObject("Part::Cylinder", name)
    obj.Angle = ang
    obj.Radius = rad
    obj.Height = hei

    DOC.recompute()

    return obj

Hasta aquí no hay nada especialmente emocionante. Pero hay que tener en cuenta algunas peculiaridades:

¿Y ahora qué hacemos con estas geometrías?

Introduzcamos las operaciones booleanas. Como ejemplo inicial, coloque estas líneas después de my_cyl. Esto crea una función para una operación Fusion también conocida como Union:

def fuse_obj(name, obj_0, obj_1):
    """Fuse two objects."""
    obj = DOC.addObject("Part::Fuse", name)
    obj.Base = obj_0
    obj.Tool = obj_1
    obj.Refine = True
    DOC.recompute()

    return obj

Nada excepcional, por ahora; sin embargo, cabe destacar la uniformidad en la codificación de las funciones. Este enfoque es más lineal que el que se observa en otros tutoriales de scripting, y esta linealidad facilita enormemente la legibilidad y las operaciones de cortar, copiar y pegar.

Vamos a usar ahora las geometrías. Elimine las líneas debajo de la sección de código que comienzan con # objects definition, e inserte las siguientes líneas:

# objects definition

obj = my_box("test_cube", 5, 5, 5)

obj1 = my_cyl("test_cyl", 360, 2, 10)

fuse_obj("Fusion", obj, obj1)

set_view()

Ejecute el script con la flecha verde y verá en la vista 3D algo como esto:

Cube and cylinder

Colocación

El concepto de colocación es relativamente complejo; consulte Tutorial Aeroplano para obtener una explicación más detallada.

Por lo general, al construir objetos complejos, necesitamos colocar las geometrías unas con respecto a otras. Este procedimientos es una tarea recurrente. La forma más común es usar la propiedad Placement de geometría.

FreeCAD ofrece una amplia variedad de maneras para configurar esta propiedad; cada una de las cuales se adapta a los conocimientos y la experiencia del usuario. La forma más sencilla de escribirla se explica en el tutorial citado, que utiliza una definición particular de la parte Rotation de Placement: bastante fácil de aprender.

FreeCAD.Placement(Vector(0, 0, 0), FreeCAD.Rotation(10, 20, 30), Vector(0, 0, 0))

Pero, por encima de otras consideraciones, un aspecto crucial es, el punto de referencia geométrico, es decir, el punto desde el cual FreeCAD modela el objeto, como se describe en esta tabla, copiada de Placement:

Object Reference Point
Part.Box vértice izquierdo (minx), frontal (miny), inferior (minz)
Part.Sphere centro de la esfera
Part.Cylinder centro de la cara inferior
Part.Cone centro de la cara inferior (o vértice si el radio inferior es 0)
Part.Torus centro del toroide
Funciones derivadas de bocetos la función hereda la posición del boceto subyacente. Los bocetos siempre comienzan con la posición = (0, 0, 0). Esta posición corresponde al origen en el boceto.

Esta información debe tenerse en cuenta, especialmente cuando tengamos que aplicar una rotación.

Algunos ejemplos pueden ser útiles; elimine la función my_box y todas las líneas posteriores a la función my_cyl, y agregue el código siguiente después de la función my_cyl:

def my_sphere(name, rad):
    """Create a Sphere."""
    obj = DOC.addObject("Part::Sphere", name)
    obj.Radius = rad

    DOC.recompute()

    return obj

def my_box2(name, len, wid, hei, cent=False, off_z=0):
    """Create a box with an optional z offset."""
    obj_b = DOC.addObject("Part::Box", name)
    obj_b.Length = len
    obj_b.Width = wid
    obj_b.Height = hei

    if cent is True:
        pos = Vector(len * -0.5, wid * -0.5, off_z)
    else:
        pos = Vector(0, 0, off_z)

    obj_b.Placement = Placement(pos, ROT0, VEC0)

    DOC.recompute()

    return obj_b

def mfuse_obj(name, objs):
    """Fuse multiple objects."""
    obj = DOC.addObject("Part::MultiFuse", name)
    obj.Shapes = objs
    obj.Refine = True
    DOC.recompute()

    return obj

def airplane():
    """Create an airplane shaped solid."""
    fuselage_length = 30
    fuselage_diameter = 5
    wing_span = fuselage_length * 1.75
    wing_width = 7.5
    wing_thickness = 1.5
    tail_height = fuselage_diameter * 3.0
    tail_position = fuselage_length * 0.70
    tail_offset = tail_position - (wing_width * 0.5)

    obj1 = my_cyl("main_body", 360, fuselage_diameter, fuselage_length)

    obj2 = my_box2("wings", wing_span, wing_thickness, wing_width, True, tail_offset)

    obj3 = my_sphere("nose", fuselage_diameter)
    obj3.Placement = Placement(Vector(0, 0, fuselage_length), ROT0, VEC0)

    obj4 = my_box2("tail", wing_thickness, tail_height, wing_width, False, 0)
    obj4.Placement = Placement(Vector(0, tail_height * -1, 0), ROT0, VEC0)

    objs = (obj1, obj2, obj3, obj4)

    obj = mfuse_obj("airplane", objs)
    obj.Placement = Placement(VEC0, Rotation(0, 0, -90), Vector(0, 0, tail_position))

    DOC.recompute()

    return obj

# objects definition

airplane()

set_view()

Vamos a explicar algo del código anterior:

Ejemplo de avión
thumb|center|Avión girado
Propiedad de ubicación

Se puede observar fácilmente que la geometría del "avión" gira alrededor de su "baricentro" o "centro de gravedad", que se ha fijado en el centro del ala, un lugar relativamente "natural", pero que podría ubicarse donde se desee.

El primer Vector(0, 0, 0) es el vector de traslación, que no se usa aquí, pero si sustituye airplane() con estas líneas:

obj_f = airplane()

print(obj_F.Placement)

En la ventana de informe verá este texto:

Placement [Pos=(0, -21, 21), Yaw-Pitch-Roll=(0, 0, -90)]

¿Qué ha pasado?

FreeCAD ha traducido la definición Vector(0, 0, 0), FreeCAD.Rotation(0, 0, -90), Vector(0, 0, tail_position) en otras palabras, nuestra definición Placement que especifica tres componentes, Traslación, Rotación y centro de rotación en los valores "internos" de solo dos componentes, Translación y Rotación.

Puede visualizar fácilmente el valor de tail_position usando una instrucción print en la función airplane() y verá que es:

tail_position = 21.0

En otras palabras, el centro de rotación de la geometría está en Vector(0, 0, 21), pero este centro de rotación no se muestra en la Interfaz gráfica de Usuario (GUI), podría ingresarse como un valor Placement, pero no podría recuperarse fácilmente.

Este es el significado de la palabra "awkward" que se ha usado para definir la propiedad Placement.


Este es el ejemplo de código completo formado por una cadena de documentación de script estandarizada que sigue la Convención de cadenas de documentación de Google:

"""Sample code.

Filename:
   airplane.py

Author:
    Dormeletti Carlo (onekk)

Version:
    1.0

License:
    Creative Commons Attribution 3.0

Summary:
    This is sample code written for a FreeCAD Wiki page.
    It creates an airplane shaped solid using standard "Part WB" shapes.

"""

import FreeCAD
from FreeCAD import Placement, Rotation, Vector
import FreeCADGui

DOC_NAME = "Wiki_Example"
DOC = FreeCAD.newDocument(DOC_NAME)
FreeCAD.setActiveDocument(DOC.Name)

ROT0 = Rotation(0, 0, 0)
VEC0 = Vector(0, 0, 0)

# Helper function

def set_view():
    """Rearrange View."""
    if not FreeCAD.GuiUp:
        return
    doc = FreeCADGui.ActiveDocument
    if doc is None:
        return
    view = doc.ActiveView
    if view is None:
        return
    # Check if the view is a 3D view:
    if not hasattr(view, "getSceneGraph"):
        return
    view.viewAxometric()
    view.fitAll()

# Script functions

def my_cyl(name, ang, rad, hei):
    """Create a Cylinder."""
    obj = DOC.addObject("Part::Cylinder", name)
    obj.Angle = ang
    obj.Radius = rad
    obj.Height = hei

    DOC.recompute()

    return obj

def my_sphere(name, rad):
    """Create a Sphere."""
    obj = DOC.addObject("Part::Sphere", name)
    obj.Radius = rad

    DOC.recompute()

    return obj

def my_box2(name, len, wid, hei, cent=False, off_z=0):
    """Create a box with an optional z offset."""
    obj_b = DOC.addObject("Part::Box", name)
    obj_b.Length = len
    obj_b.Width = wid
    obj_b.Height = hei

    if cent is True:
        pos = Vector(len * -0.5, wid * -0.5, off_z)
    else:
        pos = Vector(0, 0, off_z)

    obj_b.Placement = Placement(pos, ROT0, VEC0)

    DOC.recompute()

    return obj_b

def mfuse_obj(name, objs):
    """Fuse multiple objects."""
    obj = DOC.addObject("Part::MultiFuse", name)
    obj.Shapes = objs
    obj.Refine = True
    DOC.recompute()

    return obj

def airplane():
    """Create an airplane shaped solid."""
    fuselage_length = 30
    fuselage_diameter = 5
    wing_span = fuselage_length * 1.75
    wing_width = 7.5
    wing_thickness = 1.5
    tail_height = fuselage_diameter * 3.0
    tail_position = fuselage_length * 0.70
    tail_offset = tail_position - (wing_width * 0.5)

    obj1 = my_cyl("main_body", 360, fuselage_diameter, fuselage_length)

    obj2 = my_box2("wings", wing_span, wing_thickness, wing_width, True, tail_offset)

    obj3 = my_sphere("nose", fuselage_diameter)
    obj3.Placement = Placement(Vector(0, 0, fuselage_length), ROT0, VEC0)

    obj4 = my_box2("tail", wing_thickness, tail_height, wing_width, False, 0)
    obj4.Placement = Placement(Vector(0, tail_height * -1, 0), ROT0, VEC0)

    objs = (obj1, obj2, obj3, obj4)

    obj = mfuse_obj("airplane", objs)
    obj.Placement = Placement(VEC0, Rotation(0, 0, -90), Vector(0, 0, tail_position))

    DOC.recompute()

    return obj

# objects definition

airplane()

set_view()