| Topic |
|---|
| Scripting |
| Level |
| Básico |
| Time to complete |
| Authors |
| onekk Carlo |
| FreeCAD version |
| 0.19 |
| Example files |
| See also |
| None |
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.
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:
import FreeCAD Esta línea importa FreeCAD en el intérprete de Python de FreeCAD. Puede parecer redundante, pero no lo es.from FreeCAD import Placement, Rotation, Vector Placement Rotation y Vector se utilizan ampliamente en la programación de scripts de FreeCAD. Importarlos de esta manera le ahorrará tener que invocarlos con FreeCAD.Vector o FreeCAD.Placement en lugar de Vector o Placement. Esto le ahorrará muchas pulsaciones de teclas y hará que las líneas de código sean mucho más cortas.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.

¿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:
App., presente en muchos fragmentos de código de la documentación, es deliberada. Este código podría usarse incluso invocando FreeCAD como un módulo en un intérprete de Python externo. Si bien no es fácil hacerlo con una AppImage, con cuidado se podría lograr. Además, siguiendo el lema estándar de Python de que "mejor explícito que implícito", App. explica de forma muy deficiente el origen de los elementos.DOC = FreeCAD.activeDocument(); activeDocument no es una "constante" en sentido estricto, pero semánticamente es nuestro "Documento activo", que para nuestro uso sí es una "constante" propiamente dicha. Por eso, en Python se utiliza la convención de escribir las "constantes" en mayúsculas, sin mencionar que DOC es mucho más corto que FreeCAD.activeDocument().Placement. Al usar geometrías simples para crear geometrías más complejas, gestionar Placement resultaba engorroso.¿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:

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:
Shapes. Hemos pasado una tupla como argumentos, pero también acepta una lista.Placement dentro de la función y, antes de devolver las geometrías finales, hemos utilizado una propiedad Rotation con la escritura Yaw-Pitch-Roll. Nótese el último Vector(0, 0, tail_position), que define un 'centro de rotación de toda la geometría.![]() |
thumb|center|Avión girado |
![]() |
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()