FreeCAD Logo FreeCAD 1.0
  • Inglés Africano Árabe Bielorruso Catalán Checo Alemán Griego Español Español Euskera Finlandés Filipino Francés Gallego Croata Húngaro Indonesio Italiano Japonés Cabilio Coreano Lituano Neerlandés bokmal de Noruega Polaco Portugués Portugués Rumano Ruso Eslovaco Esloveno Serbio Sueco Turco Ucraniano Valenciano Vietnamita Chino Chino
  • Características
  • Descarga
  • Blog
  • Documentación
    Índice de documentación Primeros pasos Documentacion para el usuario El manual de FreeCAD Documentación de entornos de trabajo Documentación sobre FreeCAD + Python Documentación de codificación de C++ Tutoriales Preguntas frecuentes Política de privacidad Acerca de FreeCAD
  • Contribuir
    ¿Cómo puedo ayudar? Sponsor Informar de un error Hacer un pull request Trabajos y financiación Pautas de contribución Manual de desarrolladores Traducciones
  • Comunidad
    Código de conducta Foro The FPA GitHub GitLab Codeberg Mastodon Matrix IRC IRC via Webchat Gitter Discord Reddit Twitter Facebook LinkedIn Calendario
  • ♥ Donate

Donate

$
Información de SEPA
Configure su transferencia bancaria SEPA a:
Beneficiary: The FreeCAD project association
IBAN: BE04 0019 2896 4531
BIC/SWIFT: GEBABEBBXXX
Agencia bancaria: BNP Paribas Fortis
Dirección: Rue de la Station 64, 1360 Perwez, Belgium

While Stripe doesn't support monthly donations, you can still become a sponsor! Simply make a one-time donation equivalent to 12 months of support, and you'll gain access to the corresponding sponsoring tier. It's an easy and flexible way to contribute.

If you are not sure or not able to commit to a regular donation, but still want to help the project, you can do a one-time donation, of any amount.

Choose freely the amount you wish to donate one time only.

You can support FreeCAD by sponsoring it as an individual or organization through various platforms. Sponsorship provides a steady income for developers, allowing the FPA to plan ahead and enabling greater investment in FreeCAD. To encourage sponsorship, we offer different tiers, and unless you choose to remain anonymous, your name or company logo will be featured on our website accordingly.

from 1 USD / 1 EUR per month. You will not have your name displayed here, but you will have helped the project a lot anyway. Together, normal sponsors maintain the project on its feet as much as the bigger sponsors.

from 25 USD / 25 EUR per month. Your name or company name is displayed on this page.

from 100 USD / 100 EUR per month. Your name or company name is displayed on this page, with a link to your website, and a one-line description text.

from 200 USD / 200 EUR per month. Your name or company name and logo displayed on this page, with a link to your website and a custom description text. Companies that have helped FreeCAD early on also appear under Gold sponsors.

Instead of donating each month, you might find it more comfortable to make a one-time donation that, when divided by twelve, would give you right to enter a sponsoring tier. Don't hesitate to do so!

Choose freely the amount you wish to donate each month.

Please inform your forum name or twitter handle as a notein your transfer, or reach to us, so we can give you proper credits!

Topological data scripting
Scenegraph
Index

Introducción

Además de los tipos de objetos estándar, como anotaciones, mallas y piezas, FreeCAD también ofrece la increíble posibilidad de crear objetos paramétricos completamente programados en Python, denominados Funciones de Python. Estos objetos se comportan exactamente igual que cualquier otro objeto de FreeCAD y se guardan y restauran automáticamente al guardar o cargar archivos.

Es importante tener en cuenta una particularidad: por motivos de seguridad, los archivos de FreeCAD nunca contienen código incrustado. El código Python que se escribe para crear objetos paramétricos nunca se guarda dentro del archivo. Esto significa que si se abre un archivo que contiene dicho objeto en otro equipo, si ese código Python no está disponible en dicho equipo, el objeto no se recreará por completo. Si se distribuyen estos objetos a otros, también será necesario distribuir el script de Python, por ejemplo, como una macro.

Nota: Es posible empaquetar código Python dentro de un archivo FreeCAD utilizando la serialización JSON con un App::PropertyPythonObject, pero ese código nunca se puede ejecutar directamente y, por lo tanto, tiene poca utilidad para nuestro propósito aquí.

Las Funcionalidades Python siguen las mismas reglas que todas las Funcionalidades de FreeCAD: se separan en las partes de App y GUI. La parte de App, el objeto documento, define la geometría de nuestro objeto, mientras que su parte GUI, el objeto proveedor de vistas, define el modo en que el objeto se dibujará en la pantalla. El objeto proveedor de vistas, como cualquier otra Funcionalidad de FreeCAD, sólo está disponible cuando se ejecuta FreeCAD en su propio GUI. Hay varias propiedades y métodos disponibles para construir el objeto. Las propiedades tienen que ser de cualquiera de los tipos predefinidos entre las propiedades que ofrece FreeCAD, y aparecerán en la ventana de vista de propiedades, por lo que puede ser editadas por el usuario. De esta manera, los objetos Funcionalidad Python son total y absolutamente paramétricos. Además, puede definir las propiedades del objeto y de su ViewObject por separado.

Ejemplo básico

El siguiente ejemplo se puede encontrar en el archivo src/Mod/TemplatePyMod/FeaturePython.py, junto con otros ejemplos:

'''Examples for a feature class and its view provider.'''

import FreeCAD, FreeCADGui
from pivy import coin

class Box:
    def __init__(self, obj):
        '''Add some custom properties to our box feature'''
        obj.addProperty("App::PropertyLength", "Length", "Box", "Length of the box").Length = 1.0
        obj.addProperty("App::PropertyLength", "Width", "Box", "Width of the box").Width = 1.0
        obj.addProperty("App::PropertyLength", "Height", "Box", "Height of the box").Height = 1.0
        obj.Proxy = self

    def onChanged(self, fp, prop):
        '''Do something when a property has changed'''
        FreeCAD.Console.PrintMessage("Change property: " + str(prop) + "\n")

    def execute(self, fp):
        '''Do something when doing a recomputation, this method is mandatory'''
        FreeCAD.Console.PrintMessage("Recompute Python Box feature\n")

class ViewProviderBox:
    def __init__(self, obj):
        '''Set this object to the proxy object of the actual view provider'''
        obj.addProperty("App::PropertyColor","Color", "Box", "Color of the box").Color = (1.0, 0.0, 0.0)
        obj.Proxy = self

    def attach(self, obj):
        '''Setup the scene sub-graph of the view provider, this method is mandatory'''
        self.shaded = coin.SoGroup()
        self.wireframe = coin.SoGroup()
        self.scale = coin.SoScale()
        self.color = coin.SoBaseColor()

        data=coin.SoCube()
        self.shaded.addChild(self.scale)
        self.shaded.addChild(self.color)
        self.shaded.addChild(data)
        obj.addDisplayMode(self.shaded, "Shaded");
        style=coin.SoDrawStyle()
        style.style = coin.SoDrawStyle.LINES
        self.wireframe.addChild(style)
        self.wireframe.addChild(self.scale)
        self.wireframe.addChild(self.color)
        self.wireframe.addChild(data)
        obj.addDisplayMode(self.wireframe, "Wireframe");
        self.onChanged(obj,"Color")

    def updateData(self, fp, prop):
        '''If a property of the handled feature has changed we have the chance to handle this here'''
        # fp is the handled feature, prop is the name of the property that has changed
        l = fp.getPropertyByName("Length")
        w = fp.getPropertyByName("Width")
        h = fp.getPropertyByName("Height")
        self.scale.scaleFactor.setValue(float(l), float(w), float(h))
        pass

    def getDisplayModes(self,obj):
        '''Return a list of display modes.'''
        modes=[]
        modes.append("Shaded")
        modes.append("Wireframe")
        return modes

    def getDefaultDisplayMode(self):
        '''Return the name of the default display mode. It must be defined in getDisplayModes.'''
        return "Shaded"

    def setDisplayMode(self,mode):
        '''Map the display mode defined in attach with those defined in getDisplayModes.\
                Since they have the same names nothing needs to be done. This method is optional'''
        return mode

    def onChanged(self, vp, prop):
        '''Here we can do something when a single property got changed'''
        FreeCAD.Console.PrintMessage("Change property: " + str(prop) + "\n")
        if prop == "Color":
            c = vp.getPropertyByName("Color")
            self.color.rgb.setValue(c[0], c[1], c[2])

    def getIcon(self):
        '''Return the icon in XPM format which will appear in the tree view. This method is\
                optional and if not defined a default icon is shown.'''
        return """
            /* XPM */
            static const char * ViewProviderBox_xpm[] = {
            "16 16 6 1",
            "   c None",
            ".  c #141010",
            "+  c #615BD2",
            "@  c #C39D55",
            "#  c #000000",
            "$  c #57C355",
            "        ........",
            "   ......++..+..",
            "   .@@@@.++..++.",
            "   .@@@@.++..++.",
            "   .@@  .++++++.",
            "  ..@@  .++..++.",
            "###@@@@ .++..++.",
            "##$.@@$#.++++++.",
            "#$#$.$$$........",
            "#$$#######      ",
            "#$$#$$$$$#      ",
            "#$$#$$$$$#      ",
            "#$$#$$$$$#      ",
            " #$#$$$$$#      ",
            "  ##$$$$$#      ",
            "   #######      "};
            """

    def dumps(self):
        '''When saving the document this object gets stored using Python's json module.\
                Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\
                to return a tuple of all serializable objects or None.'''
        return None

    def loads(self,state):
        '''When restoring the serialized object from document we have the chance to set some internals here.\
                Since no data were serialized nothing needs to be done here.'''
        return None

def makeBox():
    FreeCAD.newDocument()
    a=FreeCAD.ActiveDocument.addObject("App::FeaturePython", "Box")
    Box(a)
    ViewProviderBox(a.ViewObject)

makeBox()

Aspectos a tener en cuenta

Si su objeto requiere ser recalculado tan pronto como se crea, debe hacerlo manualmente en la función __init__ ya que no se llama automáticamente. Este ejemplo no lo requiere porque el método onChanged de la clase Box tiene el mismo efecto que la función execute, pero los ejemplos expuestos a continuación requieren ser recalculados antes de que se muestre algo en la vista 3D. En los ejemplos, esto se hace manualmente con ActiveDocument.recompute() pero en escenarios más complejos debe decidir dónde recalcular, ya sea todo el documento o el objeto FeaturePython.

Este ejemplo genera varios rastros de pila de excepciones en la ventana de vista de informe. Esto se debe a que el método onChanged de la clase Box se llama cada vez que se agrega una propiedad en __init__. Cuando se agrega la primera, las propiedades Width y Height aún no existen, por lo que el intento de acceder a ellas falla.

Una explicación de __getstate__ y __setstate__ que han sido reemplazados por dumps y loads está en el hilo del foro obj.Proxy.Type es un diccionario, no una cadena.

obj.addProperty(...) devuelve obj, de modo que el valor de la propiedad se puede establecer en la misma línea:

obj.addProperty("App::PropertyLength", "Longitud", "Caja", "Longitud de la caja").Longitud = 1.0

Lo que es equivalente a:

obj.addProperty("App::PropertyLength", "Length", "Box", "Length of the box")
obj.Length = 1.0

Otro ejemplo más complejo

Este ejemplo utiliza el módulo Part para crear un octaedro y luego crea su representación Coin con Pivy.

Lo primero es el propio objeto del documento:

import FreeCAD, FreeCADGui, Part
import pivy
from pivy import coin

class Octahedron:
  def __init__(self, obj):
     "Add some custom properties to our box feature"
     obj.addProperty("App::PropertyLength","Length","Octahedron","Length of the octahedron").Length=1.0
     obj.addProperty("App::PropertyLength","Width","Octahedron","Width of the octahedron").Width=1.0
     obj.addProperty("App::PropertyLength","Height","Octahedron", "Height of the octahedron").Height=1.0
     obj.addProperty("Part::PropertyPartShape","Shape","Octahedron", "Shape of the octahedron")
     obj.Proxy = self

  def execute(self, fp):
     # Define six vetices for the shape
     v1 = FreeCAD.Vector(0,0,0)
     v2 = FreeCAD.Vector(fp.Length,0,0)
     v3 = FreeCAD.Vector(0,fp.Width,0)
     v4 = FreeCAD.Vector(fp.Length,fp.Width,0)
     v5 = FreeCAD.Vector(fp.Length/2,fp.Width/2,fp.Height/2)
     v6 = FreeCAD.Vector(fp.Length/2,fp.Width/2,-fp.Height/2)

     # Make the wires/faces
     f1 = self.make_face(v1,v2,v5)
     f2 = self.make_face(v2,v4,v5)
     f3 = self.make_face(v4,v3,v5)
     f4 = self.make_face(v3,v1,v5)
     f5 = self.make_face(v2,v1,v6)
     f6 = self.make_face(v4,v2,v6)
     f7 = self.make_face(v3,v4,v6)
     f8 = self.make_face(v1,v3,v6)
     shell=Part.makeShell([f1,f2,f3,f4,f5,f6,f7,f8])
     solid=Part.makeSolid(shell)
     fp.Shape = solid

  # helper mehod to create the faces
  def make_face(self,v1,v2,v3):
     wire = Part.makePolygon([v1,v2,v3,v1])
     face = Part.Face(wire)
     return face

después, conseguimos el objeto proveedor de vista, responsable de mostrar el objeto en la escena 3D:

class ViewProviderOctahedron:
  def __init__(self, obj):
     "Set this object to the proxy object of the actual view provider"
     obj.addProperty("App::PropertyColor","Color","Octahedron","Color of the octahedron").Color=(1.0,0.0,0.0)
     obj.Proxy = self

  def attach(self, obj):
     "Setup the scene sub-graph of the view provider, this method is mandatory"
     self.shaded = coin.SoGroup()
     self.wireframe = coin.SoGroup()
     self.scale = coin.SoScale()
     self.color = coin.SoBaseColor()

     self.data=coin.SoCoordinate3()
     self.face=coin.SoIndexedFaceSet()

     self.shaded.addChild(self.scale)
     self.shaded.addChild(self.color)
     self.shaded.addChild(self.data)
     self.shaded.addChild(self.face)
     obj.addDisplayMode(self.shaded,"Shaded");
     style=coin.SoDrawStyle()
     style.style = coin.SoDrawStyle.LINES
     self.wireframe.addChild(style)
     self.wireframe.addChild(self.scale)
     self.wireframe.addChild(self.color)
     self.wireframe.addChild(self.data)
     self.wireframe.addChild(self.face)
     obj.addDisplayMode(self.wireframe,"Wireframe");
     self.onChanged(obj,"Color")

  def updateData(self, fp, prop):
     "If a property of the handled feature has changed we have the chance to handle this here"
     # fp is the handled feature, prop is the name of the property that has changed
     if prop == "Shape":
        s = fp.getPropertyByName("Shape")
        self.data.point.setNum(6)
        cnt=0
        for i in s.Vertexes:
           self.data.point.set1Value(cnt,i.X,i.Y,i.Z)
           cnt=cnt+1

        self.face.coordIndex.set1Value(0,0)
        self.face.coordIndex.set1Value(1,1)
        self.face.coordIndex.set1Value(2,2)
        self.face.coordIndex.set1Value(3,-1)

        self.face.coordIndex.set1Value(4,1)
        self.face.coordIndex.set1Value(5,3)
        self.face.coordIndex.set1Value(6,2)
        self.face.coordIndex.set1Value(7,-1)

        self.face.coordIndex.set1Value(8,3)
        self.face.coordIndex.set1Value(9,4)
        self.face.coordIndex.set1Value(10,2)
        self.face.coordIndex.set1Value(11,-1)

        self.face.coordIndex.set1Value(12,4)
        self.face.coordIndex.set1Value(13,0)
        self.face.coordIndex.set1Value(14,2)
        self.face.coordIndex.set1Value(15,-1)

        self.face.coordIndex.set1Value(16,1)
        self.face.coordIndex.set1Value(17,0)
        self.face.coordIndex.set1Value(18,5)
        self.face.coordIndex.set1Value(19,-1)

        self.face.coordIndex.set1Value(20,3)
        self.face.coordIndex.set1Value(21,1)
        self.face.coordIndex.set1Value(22,5)
        self.face.coordIndex.set1Value(23,-1)

        self.face.coordIndex.set1Value(24,4)
        self.face.coordIndex.set1Value(25,3)
        self.face.coordIndex.set1Value(26,5)
        self.face.coordIndex.set1Value(27,-1)

        self.face.coordIndex.set1Value(28,0)
        self.face.coordIndex.set1Value(29,4)
        self.face.coordIndex.set1Value(30,5)
        self.face.coordIndex.set1Value(31,-1)

  def getDisplayModes(self,obj):
     "Return a list of display modes."
     modes=[]
     modes.append("Shaded")
     modes.append("Wireframe")
     return modes

  def getDefaultDisplayMode(self):
     "Return the name of the default display mode. It must be defined in getDisplayModes."
     return "Shaded"

  def setDisplayMode(self,mode):
     return mode

  def onChanged(self, vp, prop):
     "Here we can do something when a single property got changed"
     FreeCAD.Console.PrintMessage("Change property: " + str(prop) + "\n")
     if prop == "Color":
        c = vp.getPropertyByName("Color")
        self.color.rgb.setValue(c[0],c[1],c[2])

  def getIcon(self):
     return """
        /* XPM */
        static const char * ViewProviderBox_xpm[] = {
        "16 16 6 1",
        "    c None",
        ".   c #141010",
        "+   c #615BD2",
        "@   c #C39D55",
        "#   c #000000",
        "$   c #57C355",
        "        ........",
        "   ......++..+..",
        "   .@@@@.++..++.",
        "   .@@@@.++..++.",
        "   .@@  .++++++.",
        "  ..@@  .++..++.",
        "###@@@@ .++..++.",
        "##$.@@$#.++++++.",
        "#$#$.$$$........",
        "#$$#######      ",
        "#$$#$$$$$#      ",
        "#$$#$$$$$#      ",
        "#$$#$$$$$#      ",
        " #$#$$$$$#      ",
        "  ##$$$$$#      ",
        "   #######      "};
        """

  def dumps(self):
     return None

  def loads(self,state):
     return None

Por último, una vez que nuestro objeto y su viewobject están definidos, sólo nos falta invocarlos:

FreeCAD.newDocument()
a=FreeCAD.ActiveDocument.addObject("App::FeaturePython","Octahedron")
Octahedron(a)
ViewProviderOctahedron(a.ViewObject)

Hacer que los objetos sean seleccionables

Si desea que su objeto sea seleccionable, o al menos una parte de él, al hacer clic en la ventana gráfica, debe incluir su geometría de moneda dentro de un nodo SoFCSelection. Si su objeto tiene una representación compleja, con widgets, anotaciones, etc., es posible que desee incluir solo una parte en un SoFCSelection. FreeCAD escanea constantemente todo lo que es un SoFCSelection para detectar la selección o preselección, por lo que conviene evitar sobrecargarlo con escaneos innecesarios.

Una vez que las partes del grafo de escena que se van a seleccionar se encuentran dentro de los nodos SoFCSelection, es necesario proporcionar dos métodos para gestionar la ruta de selección. Esta ruta puede ser una cadena que indique los nombres de cada elemento o una matriz de objetos del grafo de escena. Los dos métodos son getDetailPath, que convierte una ruta de cadena en una matriz de objetos del grafo de escena, y getElementPicked, que toma un elemento en el que se ha hecho clic en el grafo de escena y devuelve su nombre (no su ruta de cadena).

Aquí está el ejemplo de molécula anterior, adaptado para que los elementos de la molécula sean seleccionables:

class Molecule:
    def __init__(self, obj):
        ''' Add two point properties '''
        obj.addProperty("App::PropertyVector","p1","Line","Start point")
        obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(5,0,0)

        obj.Proxy = self

    def onChanged(self, fp, prop):
        if prop == "p1" or prop == "p2":
            ''' Print the name of the property that has changed '''
            fp.Shape = Part.makeLine(fp.p1,fp.p2)

    def execute(self, fp):
        ''' Print a short message when doing a recomputation, this method is mandatory '''
        fp.Shape = Part.makeLine(fp.p1,fp.p2)

class ViewProviderMolecule:
    def __init__(self, obj):
        ''' Set this object to the proxy object of the actual view provider '''
        obj.Proxy = self
        self.ViewObject = obj
        sep1=coin.SoSeparator()
        sel1 = coin.SoType.fromName('SoFCSelection').createInstance()
        # sel1.policy.setValue(coin.SoSelection.SHIFT)
        sel1.ref()
        sep1.addChild(sel1)
        self.trl1=coin.SoTranslation()
        sel1.addChild(self.trl1)
        sel1.addChild(coin.SoSphere())
        sep2=coin.SoSeparator()
        sel2 = coin.SoType.fromName('SoFCSelection').createInstance()
        sel2.ref()
        sep2.addChild(sel2)
        self.trl2=coin.SoTranslation()
        sel2.addChild(self.trl2)
        sel2.addChild(coin.SoSphere())
        obj.RootNode.addChild(sep1)
        obj.RootNode.addChild(sep2)
        self.updateData(obj.Object, 'p2')
        self.sel1 = sel1
        self.sel2 = sel2

    def getDetailPath(self, subname, path, append):
        vobj = self.ViewObject
        if append:
            path.append(vobj.RootNode)
            path.append(vobj.SwitchNode)

            mode = vobj.SwitchNode.whichChild.getValue()
            if mode >= 0:
                mode = vobj.SwitchNode.getChild(mode)
                path.append(mode)
                sub = Part.splitSubname(subname)[-1]
                if sub == 'Atom1':
                    path.append(self.sel1)
                elif sub == 'Atom2':
                    path.append(self.sel2)
                else:
                    path.append(mode.getChild(0))
        return True

    def getElementPicked(self, pp):
        path = pp.getPath()
        if path.findNode(self.sel1) >= 0:
            return 'Atom1'
        if path.findNode(self.sel2) >= 0:
            return 'Atom2'
        raise NotImplementedError

    def updateData(self, fp, prop):
        "If a property of the handled feature has changed we have the chance to handle this here"
        # fp is the handled feature, prop is the name of the property that has changed
        if prop == "p1":
            p = fp.getPropertyByName("p1")
            self.trl1.translation=(p.x,p.y,p.z)
        elif prop == "p2":
            p = fp.getPropertyByName("p2")
            self.trl2.translation=(p.x,p.y,p.z)

    def dumps(self):
        return None

    def loads(self,state):
        return None

def makeMolecule():
    FreeCAD.newDocument()
    a=FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Molecule")
    Molecule(a)
    ViewProviderMolecule(a.ViewObject)
    FreeCAD.ActiveDocument.recompute()

Trabajando con formas simples

Si su objeto paramétrico genera una forma, no necesita usar un objeto View Provider. La forma se mostrará utilizando la representación de forma estándar de FreeCAD:

import FreeCAD as App
import FreeCADGui
import FreeCAD
import Part
class Line:
    def __init__(self, obj):
        '''"App two point properties" '''
        obj.addProperty("App::PropertyVector","p1","Line","Start point")
        obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(1,0,0)
        obj.Proxy = self

    def execute(self, fp):
        '''"Print a short message when doing a recomputation, this method is mandatory" '''
        fp.Shape = Part.makeLine(fp.p1,fp.p2)

a=FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Line")
Line(a)
a.ViewObject.Proxy=0 # just set it to something different from None (this assignment is needed to run an internal notification)
FreeCAD.ActiveDocument.recompute()

Same code with use ViewProviderLine

import FreeCAD as App
import FreeCADGui
import FreeCAD
import Part

class Line:
    def __init__(self, obj):
         '''"App two point properties" '''
         obj.addProperty("App::PropertyVector","p1","Line","Start point")
         obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(100,0,0)
         obj.Proxy = self

    def execute(self, fp):
        '''"Print a short message when doing a recomputation, this method is mandatory" '''
        fp.Shape = Part.makeLine(fp.p1,fp.p2)

class ViewProviderLine:
   def __init__(self, obj):
      ''' Set this object to the proxy object of the actual view provider '''
      obj.Proxy = self

   def getDefaultDisplayMode(self):
      ''' Return the name of the default display mode. It must be defined in getDisplayModes. '''
      return "Flat Lines"

a=FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Line")
Line(a)
ViewProviderLine(a.ViewObject)
App.ActiveDocument.recompute()

Estructura del grafo de escena

Es posible que haya notado que los ejemplos anteriores construyen sus grafos de escena de maneras ligeramente diferentes. Algunos usan obj.addDisplayMode(node, "modename") mientras que otros usan obj.SwitchNode.getChild(x).addChild(y).

Cada elemento de un documento de FreeCAD se basa en la siguiente estructura de grafo de escena:

RootNode
 \- SwitchNode
     \- Shaded
      - Wireframe
      - etc

El SwitchNode muestra solo uno de sus hijos, dependiendo del modo de visualización seleccionado en FreeCAD.

Los ejemplos que utilizan addDisplayMode construyen sus grafos de escena únicamente a partir de elementos de grafos de escena de coin3d. Internamente, addDisplayMode agrega un nuevo hijo al SwitchNode. El nombre de ese nodo coincidirá con el modo de visualización que se le pasó.

Los ejemplos que utilizan SwitchNode.getChild(x).addChild también construyen parte de su geometría utilizando funciones del Part Workbench, como fp.Shape = Part.makeLine(fp.p1,fp.p2). Esto construye los diferentes grafos de escena del modo de visualización bajo el SwitchNode; cuando más adelante agreguemos elementos coin3d al grafo de escena, necesitaremos agregarlos a los grafos de escena del modo de visualización existentes utilizando addChild en lugar de crear un nuevo hijo del SwitchNode.

Al usar addDisplayMode() para agregar geometría al grafo de escena, cada modo de visualización debe tener su propio nodo, el cual se pasa a addDisplayMode(). No reutilice el mismo nodo para esto. Hacerlo confundirá al mecanismo de selección. No hay problema si el nodo de cada modo de visualización tiene los mismos nodos de geometría agregados debajo, solo la raíz de cada modo de visualización debe ser distinta.

Aquí está el ejemplo de molécula anterior, adaptado para ser dibujado solo con objetos de grafo de escena de Coin3D en lugar de usar objetos del Entorno de trabajo de piezas:

import Part
from pivy import coin

class Molecule:
    def __init__(self, obj):
        ''' Add two point properties '''
        obj.addProperty("App::PropertyVector","p1","Line","Start point")
        obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(5,0,0)

        obj.Proxy = self

    def onChanged(self, fp, prop):
        pass

    def execute(self, fp):
        ''' Print a short message when doing a recomputation, this method is mandatory '''
        pass

class ViewProviderMolecule:
    def __init__(self, obj):
        ''' Set this object to the proxy object of the actual view provider '''
        self.constructed = False
        obj.Proxy = self
        self.ViewObject = obj

    def attach(self, obj):
        material = coin.SoMaterial()
        material.diffuseColor = (1.0, 0.0, 0.0)
        material.emissiveColor = (1.0, 0.0, 0.0)
        drawStyle = coin.SoDrawStyle()
        drawStyle.pointSize.setValue(10)
        drawStyle.style = coin.SoDrawStyle.LINES
        wireframe = coin.SoGroup()
        shaded = coin.SoGroup()
        self.wireframe = wireframe
        self.shaded = shaded

        self.coords = coin.SoCoordinate3()
        self.coords.point.setValues(0, 2, [FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(1, 0, 0)])
        wireframe += self.coords
        wireframe += drawStyle
        wireframe += material
        shaded += self.coords
        shaded += drawStyle
        shaded += material

        g = coin.SoGroup()
        sel1 = coin.SoType.fromName('SoFCSelection').createInstance()
        sel1.style = 'EMISSIVE_DIFFUSE'
        p1 = coin.SoType.fromName('SoIndexedPointSet').createInstance()
        p1.coordIndex.set1Value(0, 0)
        sel1 += p1
        g += sel1
        wireframe += g
        shaded += g

        g = coin.SoGroup()
        sel2 = coin.SoType.fromName('SoFCSelection').createInstance()
        sel2.style = 'EMISSIVE_DIFFUSE'
        p2 = coin.SoType.fromName('SoIndexedPointSet').createInstance()
        p2.coordIndex.set1Value(0, 1)
        sel2 += p2
        g += sel2
        wireframe += g
        shaded += g

        g = coin.SoGroup()
        sel3 = coin.SoType.fromName('SoFCSelection').createInstance()
        sel3.style = 'EMISSIVE_DIFFUSE'
        p3 = coin.SoType.fromName('SoIndexedLineSet').createInstance()
        p3.coordIndex.setValues(0, 2, [0, 1])
        sel3 += p3
        g += sel3
        wireframe += g
        shaded += g

        obj.addDisplayMode(wireframe, 'Wireframe')
        obj.addDisplayMode(shaded, 'Shaded')

        self.sel1 = sel1
        self.sel2 = sel2
        self.sel3 = sel3
        self.constructed = True
        self.updateData(obj.Object, 'p2')

    def getDetailPath(self, subname, path, append):
        vobj = self.ViewObject
        if append:
            path.append(vobj.RootNode)
            path.append(vobj.SwitchNode)

            mode = vobj.SwitchNode.whichChild.getValue()
            FreeCAD.Console.PrintWarning("getDetailPath: mode {} is active\n".format(mode))
            if mode >= 0:
                mode = vobj.SwitchNode.getChild(mode)
                path.append(mode)
                sub = Part.splitSubname(subname)[-1]
                print(sub)
                if sub == 'Atom1':
                    path.append(self.sel1)
                elif sub == 'Atom2':
                    path.append(self.sel2)
                elif sub == 'Line':
                    path.append(self.sel3)
                else:
                    path.append(mode.getChild(0))
        return True

    def getElementPicked(self, pp):
        path = pp.getPath()
        if path.findNode(self.sel1) >= 0:
            return 'Atom1'
        if path.findNode(self.sel2) >= 0:
            return 'Atom2'
        if path.findNode(self.sel3) >= 0:
            return 'Line'
        raise NotImplementedError

    def updateData(self, fp, prop):
        "If a property of the handled feature has changed we have the chance to handle this here"
        # fp is the handled feature, prop is the name of the property that has changed
        if not self.constructed:
            return
        if prop == "p1":
            p = fp.getPropertyByName("p1")
            self.coords.point.set1Value(0, p)
        elif prop == "p2":
            p = fp.getPropertyByName("p2")
            self.coords.point.set1Value(1, p)

    def getDisplayModes(self, obj):
        return ['Wireframe', 'Shaded']

    def getDefaultDisplayMode(self):
        return 'Shaded'

    def setDisplayMode(self, mode):
        return mode

    def dumps(self):
        return None

    def loads(self,state):
        return None

def makeMolecule():
    FreeCAD.newDocument()
    a=FreeCAD.ActiveDocument.addObject("App::FeaturePython","Molecule")
    Molecule(a)
    b=ViewProviderMolecule(a.ViewObject)
    a.touch()
    FreeCAD.ActiveDocument.recompute()
    return a,b

a,b = makeMolecule()

Objetos script en Part Desing

Al crear objetos con script en PartDesign, el proceso es similar al descrito anteriormente, pero con algunas consideraciones adicionales. Debemos gestionar dos propiedades de forma: una para la forma que se visualiza en la vista 3D y otra para la forma utilizada por las herramientas de patrones, como las funciones de patrón polar. Las formas de los objetos también deben fusionarse con cualquier material existente en el cuerpo (o recortarse de él en el caso de las funciones sustractivas). Además, debemos tener en cuenta la colocación y la fijación de los objetos de forma ligeramente diferente.

Las funciones de objetos sólidos con script de PartDesign deben basarse en PartDesign::FeaturePython, PartDesign::FeatureAdditivePython o PartDesign::FeatureSubtractivePython, en lugar de Part::FeaturePython. Solo las variantes Additive y Subtractive pueden usarse en funciones de patrón, y si se basan en Part::FeaturePython, cuando el usuario arrastra el objeto a un cuerpo de PartDesign, se convierte en una BaseFeature en lugar de ser tratado por el cuerpo como un objeto nativo de PartDesign. Nota: se espera que todos estos sean sólidos, por lo que si está creando una función que no sea sólida, debe basarse en Part::FeaturePython; de lo contrario, la siguiente función en el árbol intentará fusionarse como un sólido y fallará.

Aquí tiene un ejemplo sencillo de cómo crear un tubo primitivo, similar al que aparece en Part Workbench, pero este será un objeto sólido de PartDesign. Para ello, utilizaremos dos archivos separados: pdtube.FCMacro y pdtube.py. El usuario ejecutará el archivo .FCMacro para crear el objeto. El archivo .py contendrá las definiciones de clase, importadas por .FCMacro. El motivo de este método es mantener la naturaleza paramétrica del objeto tras reiniciar FreeCAD y abrir un documento que contenga uno de nuestros tubos.

Primero, el archivo de definición de clase:

# -*- coding: utf-8 -*-
#classes should go in pdtube.py
import FreeCAD, FreeCADGui, Part
class PDTube:
    def __init__(self,obj):
        obj.addProperty("App::PropertyLength","Radius1","Tube","Radius1").Radius1 = 5
        obj.addProperty("App::PropertyLength","Radius2","Tube","Radius2").Radius2 = 10
        obj.addProperty("App::PropertyLength","Height","Tube","Height of tube").Height = 10
        self.makeAttachable(obj)
        obj.Proxy = self

    def makeAttachable(self, obj):

        if int(FreeCAD.Version()[1]) >= 19:
            obj.addExtension('Part::AttachExtensionPython')
        else:
            obj.addExtension('Part::AttachExtensionPython', obj)

        obj.setEditorMode('Placement', 0) #non-readonly non-hidden

    def execute(self,fp):
        outer_cylinder = Part.makeCylinder(fp.Radius2, fp.Height)
        inner_cylinder = Part.makeCylinder(fp.Radius1, fp.Height)
        if fp.Radius1 == fp.Radius2: #just make cylinder
            tube_shape = outer_cylinder
        elif fp.Radius1 < fp.Radius2:
            tube_shape = outer_cylinder.cut(inner_cylinder)
        else: #invert rather than error out
            tube_shape = inner_cylinder.cut(outer_cylinder)

        if not hasattr(fp, "positionBySupport"):
            self.makeAttachable(fp)
        fp.positionBySupport()
        tube_shape.Placement = fp.Placement

        #BaseFeature (shape property of type Part::PropertyPartShape) is provided for us
        #with the PartDesign::FeaturePython and related classes, but it might be empty
        #if our object is the first object in the tree.  it's a good idea to check
        #for its existence in case we want to make type Part::FeaturePython, which won't have it

        if hasattr(fp, "BaseFeature") and fp.BaseFeature != None:
            if "Subtractive" in fp.TypeId:
                full_shape = fp.BaseFeature.Shape.cut(tube_shape)
            else:
                full_shape = fp.BaseFeature.Shape.fuse(tube_shape)
            full_shape.transformShape(fp.Placement.inverse().toMatrix(), True) #borrowed from gears workbench
            fp.Shape = full_shape
        else:
            fp.Shape = tube_shape
        if hasattr(fp,"AddSubShape"): #PartDesign::FeatureAdditivePython and
                                      #PartDesign::FeatureSubtractivePython have this
                                      #property but PartDesign::FeaturePython does not
                                      #It is the shape used for copying in pattern features
                                      #for example in making a polar pattern
            tube_shape.transformShape(fp.Placement.inverse().toMatrix(), True)
            fp.AddSubShape = tube_shape

class PDTubeVP:
    def __init__(self, obj):
        '''Set this object to the proxy object of the actual view provider'''
        obj.Proxy = self

    def attach(self,vobj):
        self.vobj = vobj

    def updateData(self, fp, prop):
        '''If a property of the handled feature has changed we have the chance to handle this here'''
        pass

    def getDisplayModes(self,obj):
        '''Return a list of display modes.'''
        modes=[]
        modes.append("Flat Lines")
        modes.append("Shaded")
        modes.append("Wireframe")
        return modes

    def getDefaultDisplayMode(self):
        '''Return the name of the default display mode. It must be defined in getDisplayModes.'''
        return "Flat Lines"

    def setDisplayMode(self,mode):
        '''Map the display mode defined in attach with those defined in getDisplayModes.\
                Since they have the same names nothing needs to be done. This method is optional'''
        return mode

    def onChanged(self, vp, prop):
        '''Here we can do something when a single property got changed'''
        #FreeCAD.Console.PrintMessage("Change property: " + str(prop) + "\n")
        pass

    def getIcon(self):
        '''Return the icon in XPM format which will appear in the tree view. This method is\
                optional and if not defined a default icon is shown.'''
        return """
            /* XPM */
            static const char * ViewProviderBox_xpm[] = {
            "16 16 6 1",
            "   c None",
            ".  c #141010",
            "+  c #615BD2",
            "@  c #C39D55",
            "#  c #000000",
            "$  c #57C355",
            "        ........",
            "   ......++..+..",
            "   .@@@@.++..++.",
            "   .@@@@.++..++.",
            "   .@@  .++++++.",
            "  ..@@  .++..++.",
            "###@@@@ .++..++.",
            "##$.@@$#.++++++.",
            "#$#$.$$$........",
            "#$$#######      ",
            "#$$#$$$$$#      ",
            "#$$#$$$$$#      ",
            "#$$#$$$$$#      ",
            " #$#$$$$$#      ",
            "  ##$$$$$#      ",
            "   #######      "};
            """

    def dumps(self):
        '''When saving the document this object gets stored using Python's json module.\
                Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\
                to return a tuple of all serializable objects or None.'''
        return None

    def loads(self,state):
        '''When restoring the serialized object from document we have the chance to set some internals here.\
                Since no data were serialized nothing needs to be done here.'''
        return None

Y ahora el archivo de macro para crear el objeto:

# -*- coding: utf-8 -*-

#pdtube.FCMacro
import pdtube
#above line needed if the class definitions above are place in another file: PDTube.py
#this is needed if the tube object is to remain parametric after restarting FreeCAD and loading
#a document containing the object

body = FreeCADGui.ActiveDocument.ActiveView.getActiveObject("pdbody")
if not body:
    FreeCAD.Console.PrintError("No active body.\n")
else:
    from PySide import QtGui
    window = FreeCADGui.getMainWindow()
    items = ["Additive","Subtractive","Neither additive nor subtractive"]
    item,ok =QtGui.QInputDialog.getItem(window,"Select tube type","Select whether you want additive, subtractive, or neither:",items,0,False)
    if ok:
        if item == items[0]:
            className = "PartDesign::FeatureAdditivePython"
        elif item == items[1]:
            className = "PartDesign::FeatureSubtractivePython"
        else:
            className = "PartDesign::FeaturePython" #not usable in pattern features, such as polar pattern

        tube = FreeCAD.ActiveDocument.addObject(className,"Tube")
        pdtube.PDTube(tube)
        pdtube.PDTubeVP(tube.ViewObject)
        body.addObject(tube) #optionally we can also use body.insertObject() for placing at particular place in tree

Tipos de objetos disponibles

Los tipos de objetos que puede crear con FreeCAD.ActiveDocument.addObject() dependen de los módulos cargados. Tras cargar todos los entornos de trabajo internos, puede obtener una lista completa con FreeCAD.ActiveDocument.supportedTypes(). Solo se pueden usar para objetos programados los tipos de objetos cuyo nombre termine en Python. Estos se enumeran aquí (para FreeCAD v1.0):

  • App::DocumentObjectGroupPython
  • App::FeaturePython
  • App::GeometryPython
  • App::LinkElementPython
  • App::LinkGroupPython
  • App::LinkPython
  • App::MaterialObjectPython
  • App::PlacementPython
  • Fem::ConstraintPython
  • Fem::FeaturePython
  • Fem::FemAnalysisPython
  • Fem::FemMeshObjectPython
  • Fem::FemResultObjectPython
  • Fem::FemSolverObjectPython
  • Measure::MeasurePython
  • Mesh::FeaturePython
  • Part::CustomFeaturePython
  • Part::FeaturePython
  • Part::Part2DObjectPython
  • PartDesign::FeatureAdditivePython
  • PartDesign::FeatureAddSubPython
  • PartDesign::FeaturePython
  • PartDesign::FeatureSubtractivePython
  • PartDesign::SubShapeBinderPython
  • Path::FeatureAreaPython
  • Path::FeatureAreaViewPython
  • Path::FeatureCompoundPython
  • Path::FeaturePython
  • Path::FeatureShapePython
  • Points::FeaturePython
  • Sketcher::SketchObjectPython
  • Spreadsheet::SheetPython
  • TechDraw::DrawBrokenViewPython
  • TechDraw::DrawComplexSectionPython
  • TechDraw::DrawLeaderLinePython
  • TechDraw::DrawPagePython
  • TechDraw::DrawRichAnnoPython
  • TechDraw::DrawTemplatePython
  • TechDraw::DrawTilePython
  • TechDraw::DrawTileWeldPython
  • TechDraw::DrawViewPartPython
  • TechDraw::DrawViewPython
  • TechDraw::DrawViewSectionPython
  • TechDraw::DrawViewSymbolPython
  • TechDraw::DrawWeldSymbolPython

Métodos disponibles

Consulte FeaturePython methods para obtener la referencia completa.

Propiedades disponibles

Las propiedades son los verdaderos componentes básicos de los objetos FeaturePython. A través de ellas, podrá interactuar con su objeto y modificarlo. Después de crear un nuevo objeto FeaturePython en su documento, puede obtener una lista de las propiedades disponibles:

obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython", "Box")
obj.supportedProperties()

Consulte FeaturePython Custom Properties para obtener una descripción general.

Al agregar propiedades a sus objetos personalizados, tenga en cuenta lo siguiente:

  • No utilice los caracteres < ni > en las descripciones de las propiedades (esto dañaría los fragmentos XML del archivo .FCStd).
  • Las propiedades se almacenan alfabéticamente en un archivo .FCStd. Si incluye una forma en sus propiedades, cualquier propiedad cuyo nombre aparezca después de "Forma" en orden alfabético se cargará DESPUÉS de la forma, lo que puede provocar comportamientos inesperados.

Las propiedades se definen en el archivo de cabecera C++ PropertyStandard [1].

Atributos de la propiedad

Por defecto, el usuario puede modificar las propiedades, pero es posible configurarlas como de solo lectura, por ejemplo, si se desea mostrar el resultado de un método. También es posible ocultar una propiedad. Estos atributos se pueden configurar mediante:

obj.setEditorMode("MyPropertyName", mode)

El modo puede tener los siguientes valores:

0 -- Modo predeterminado, lectura y escritura

1 -- Solo lectura

2 -- Oculto

3 -- Solo lectura y oculto

Los atributos también se pueden configurar usando una lista de cadenas, por ejemplo, obj.setEditorMode("Placement", ["ReadOnly", "Hidden"]).

Los atributos establecidos mediante setEditorMode pueden ser eliminados por el usuario. Consulte Editor de propiedades. Tenga en cuenta que las propiedades de solo lectura aún pueden modificarse desde Python.

También puede configurar estos y otros atributos directamente con la función addProperty. Los atributos configurados con esta función no pueden ser modificados por el usuario. Una posibilidad interesante es marcar una propiedad como propiedad de salida. De esta forma, FreeCAD no marcará el objeto como modificado al cambiarlo (por lo que no será necesario recalcularlo).

Ejemplo de propiedad de salida (véase también https://forum.freecad.org/viewtopic.php?t=24928):

obj.addProperty("App::PropertyString", "MyCustomProperty", "", "", 8)

A continuación se enumeran los atributos que se pueden configurar con addProperty. Se pueden configurar varios atributos añadiendo valores.

0 -- Prop_None, Sin atributo de propiedad especial

1 -- Prop_ReadOnly, La propiedad es de solo lectura en el editor

2 -- Prop_Transient, La propiedad no se guardará en el archivo

4 -- Prop_Hidden, La propiedad no aparecerá en el editor

8 -- Prop_Output, La propiedad modificada no afecta a su contenedor principal

16 -- Prop_NoRecompute, La propiedad modificada no afecta a su contenedor para el recálculo

32 -- Prop_NoPersist, La propiedad no se guardará en el archivo

Los atributos de la propiedad se definen en el archivo de encabezado C++ PropertyContainer [2].

Para Prop_ReadOnly y Prop_Hidden la función addProperty también tiene argumentos booleanos:

obj.addProperty("App::PropertyString", "MyCustomProperty", "", "", 0, True, True)

Lo que equivale a:

obj.addProperty("App::PropertyString", "MyCustomProperty", "", "", 1+4)

introduced in 1.0: La firma completa de la función es:

obj.addProperty(type: string, name: string, group="", doc="", attr=0, read_only=False, hidden=False, enum_vals=[])
  • type: Tipo de propiedad.
  • name: Nombre de la propiedad.
  • group: Subsección de la propiedad (utilizada en el Editor de propiedades).
  • doc: Información sobre herramientas (ídem).
  • attr: Atributo, ver arriba.
  • read_only: Ver arriba.
  • hidden: Ver arriba.
  • enum_vals: Valores de enumeración (lista de cadenas), solo relevante si el tipo es "App::PropertyEnumeration".

Extensiones disponibles

La lista de extensiones disponibles se puede obtener con grep -RI EXTENSION_PROPERTY_SOURCE_TEMPLATE en el repositorio del código fuente y se proporciona aquí (para FreeCAD v1.1).

Para objetos:

  • App::GeoFeatureGroupExtensionPython
  • App::GroupExtensionPython
  • App::LinkBaseExtensionPython
  • App::LinkExtensionPython
  • App::OriginGroupExtensionPython
  • App::SuppressibleExtensionPython
  • Part::AttachExtensionPython
  • TechDraw::CosmeticExtensionPython

Para ver objetos:

  • Gui::ViewProviderExtensionPython
  • Gui::ViewProviderGeoFeatureGroupExtensionPython
  • Gui::ViewProviderGroupExtensionPython
  • Gui::ViewProviderOriginGroupExtensionPython
  • Gui::ViewProviderSuppressibleExtensionPython
  • PartGui::ViewProviderAttachExtensionPython
  • PartGui::ViewProviderGridExtensionPython
  • PartGui::ViewProviderSplineExtensionPython

Existen otras extensiones, pero no funcionan tal cual:

  • App::ExtensionPython
  • Part::PreviewExtensionPython
  • PartGui::ViewProviderPreviewExtensionPython
  • TechDrawGui::ViewProviderCosmeticExtensionPython
  • TechDrawGui::ViewProviderDrawingViewExtensionPython
  • TechDrawGui::ViewProviderPageExtensionPython
  • TechDrawGui::ViewProviderTemplateExtensionPython

Más información

Páginas adicionales:

  • Objetos con script que guardan atributos
  • Migración de objetos con script
  • Objetos con script que guardan atributos
  • Proveedores de vista

Hilos de discusión interesantes en foros sobre objetos programados:

  • Python object attributes lost at load
  • New FeaturePython is grey
  • Explanation on dumps and loads, official documentation
  • Eigenmode frequency always 0?
  • how to implement python feature's setEdit properly?

Además de los ejemplos presentados aquí, consulte el código fuente de FreeCAD src/Mod/TemplatePyMod/FeaturePython.py para obtener más ejemplos.


Programación de datos topológicos
Grafo de escena
Index

Esta página ha sido recuperada de https://wiki.freecad.org/Scripted_objects

¡Contáctanos!
Forum GitHub Mastodon Matrix IRC Gitter.im Discord Reddit Twitter Facebook LinkedIn

© El equipo de FreeCAD. Créditos de imagen de la página principal (de arriba a abajo): ppemawm, r-frank, epileftric, regis, rider_mortagnais, bejant.

Este proyecto es apoyado por: , KiCad Services Corp. y otros patrocinadores

GitHubMejora esta página en GitHub