Scripted objects saving attributes/es

Introducción

Los objetos con script se reconstruyen cada vez que se abre un documento FCStd. Para ello, el documento conserva una referencia al módulo y a la clase de Python que se utilizaron para crear el objeto, junto con sus propiedades.

Los atributos de la clase utilizada para crear el objeto también se pueden guardar, es decir, "serializar". Esto se puede controlar aún más mediante los métodos dumps y loads de la clase.

Guardando todos los atributos

Por defecto, los atributos guardados en una clase de objeto son los del diccionario __dict__ de la clase.

# various_states.py
class VariousStates:
    def __init__(self, obj):
        obj.addProperty("App::PropertyLength", "Length")
        obj.addProperty("App::PropertyArea", "Area")
        obj.Length = 15
        obj.Area = 300
        obj.Proxy = self

        Type = dict()
        Type["Version"] = "Custom"
        Type["Release"] = "production"
        self.Type = Type
        self.Type = "Custom"
        self.ver = "0.18"
        self.color = (0, 0, 1)
        self.width = 2.5

    def execute(self, obj):
        pass

Se puede crear un objeto usando esta clase y guardarlo en my_document.FCstd. Si no se asigna ningún viewprovider en particular al nuevo objeto, su clase proxy simplemente se establece en un valor diferente de None, en este caso, en 1.

import FreeCAD as App
import various_states

doc = App.newDocument()
doc.FileName = "my_document.FCStd"

obj = doc.addObject("Part::FeaturePython", "Custom")
various_states.VariousStates(obj)

if App.GuiUp:
    obj.ViewObject.Proxy = 1

doc.recompute()
doc.save()

Al volver a abrir el archivo, podemos inspeccionar el diccionario de la clase del objeto.

>>> obj = App.ActiveDocument.Custom
>>> print(obj.Proxy)
<various_states.VariousStates object at 0x7f0a899bde10>
>>> print(obj.Proxy.__dict__)
{'Type': {'Version': 'Custom', 'Release': 'production'}, 'ver': '0.18', 'color': [0, 0, 1], 'width': 2.5}

Observamos que todos los atributos que comienzan con self en la clase se guardaron. Estos pueden ser de diferentes tipos, incluyendo cadena, lista, flotante y diccionario. La tupla original para self.color se convirtió en una lista, pero por lo demás, todos los atributos se cargaron de la misma manera.

Guardando atributos específicos

Podemos definir una clase similar a la primera, que implemente atributos específicos para guardar.

# various_states.py
class CustomStates:
    def __init__(self, obj):
        obj.addProperty("App::PropertyLength", "Length")
        obj.addProperty("App::PropertyArea", "Area")
        obj.Length = 15
        obj.Area = 300
        obj.Proxy = self

        Type = dict()
        Type["Version"] = "Custom"
        Type["Release"] = "production"
        self.Type = Type
        self.ver = "0.18"
        self.color = (0, 0, 1)
        self.width = 2.5

    def execute(self, obj):
        pass

    def dumps(self):
        return self.color, self.width

    def loads(self, state):
        self.color = state[0]
        self.width = state[1]

El valor de retorno de dumps es el objeto que se serializará. Puede ser un único valor o una tupla de valores. Cuando se restaura el objeto, la clase llama al método loads, pasando la variable state con el contenido serializado. En este caso, state es una tupla que se descomprime en las variables correspondientes para reconstruir el "estado" original.

state = (self.color, self.width)
state = ((0, 0, 1), 2.5)

Podemos crear un objeto con esta clase y guardar el documento, igual que en el ejemplo anterior. Al volver a abrir el archivo, podemos consultar el diccionario de la clase del objeto.

>>> obj2 = App.ActiveDocument.Custom2
>>> print(obj2.Proxy)
<various_states.CustomStates object at 0x7fb399496630>
>>> print(obj2.Proxy.__dict__)
{'color': [0, 0, 1], 'width': 2.5}

La tupla original para self.color se convirtió en una lista, pero por lo demás la información se recuperó correctamente. En lugar de restaurar todos los atributos como en el caso anterior, solo se restauraron los atributos que especificamos en dumps y loads.

Uso

Identificación del tipo

Normalmente, los objetos con script deberían usar propiedades para almacenar información, ya que estas se restauran automáticamente cuando se abre el documento.

Sin embargo, en ocasiones la clase restaura información interna que no está destinada a ser modificada, pero que resulta útil para su inspección.

Por ejemplo, la mayoría de los objetos del Entorno de Trabajo de Pieza establecen un atributo Type que se puede utilizar para determinar el tipo de objeto que se está utilizando.

class DraftObject:
    def __init__(self, obj, _type):
        self.Type = _type

    def dumps(self):
        return self.Type

    def loads(self, state):
        if state:
            self.Type = state

Todos los objetos tienen una propiedad TypeId, pero para scripted objects esta propiedad no es útil, ya que siempre se refiere a la clase C++ principal, por ejemplo, Part::Part2DObjectPython o Part::FeaturePython. Por lo tanto, tener este atributo adicional Proxy.Type en la clase es útil para tratar cada objeto de una manera particular.

Migrando el objeto

La información de versión se puede almacenar dentro de la clase para verificar el origen de un objeto.

class CustomObject:
    def __init__(self, obj, _type):
        self.Type = _type
        self.version = "0.18"

    def dumps(self):
        return self.Type, self.version

    def loads(self, state):
        if state:
            self.Type = state[0]
            self.version = state[1]

Si la estructura de la clase cambia, es decir, si sus propiedades o métodos cambian, se renombran o se eliminan, podríamos comprobar el atributo de versión para migrar el objeto antiguo a un nuevo conjunto de propiedades o a una nueva clase. Esto se puede hacer implementando el método onDocumentRestored, como se explica en Migración de objetos mediante scripts.

class CustomObject:
    def onDocumentRestored(self, obj):
        if hasattr(obj.Proxy, "version") and obj.Proxy.version:
            if obj.Proxy.version == "0.18":
                self.migrate_from_018(obj)

    def migrate_from_018(self, obj):
        ...

Enlaces