Difference between revisions of "Scene Saver: STL"

From The Foundry MODO SDK wiki
Jump to: navigation, search
(Created page with "TriangleSoup Class")
 
Line 1: Line 1:
[[LXu_TRIANGLESOUP_(index)#C16|TriangleSoup Class]]
+
== Introduction ==
 +
 
 +
A walkthrough of the implementation of the STL scene saver plugin that ships with modo 701. Features covered include:
 +
 
 +
* [[LXu_TRIANGLESOUP_(index)#C16|TriangleSoup]] class to parse the mesh surfaces and enumerate the polgons & points
 +
* [[Saver:_Server_basics|Saver Server]] class
 +
* Use of [[Persist_(lx-persist.hpp)|Persistent Data]] and [[ILxVisitor_(index)#C85|Visitor]] classes to store and retrieve data from the user config file.
 +
* Custom Command plugin and config files to expose the persistent data as settings in modo's preferences dialog
 +
 
 +
to be continued ....
 +
 
 +
 
 +
<syntaxhighlight lang="python">
 +
# STL saver plug-in, in Python
 +
 
 +
import struct
 +
import lx
 +
import lxifc
 +
import lxu.vector
 +
import lxu.command
 +
 
 +
# these are the default units used by the target application to interpret the
 +
# dimensionless values stored in the STL file along with a conversion value. The
 +
# user will specify the unit as a prefernce setting and the scale value will be used
 +
# scale the values output to the STL file so that the object appears the right
 +
# size in the target application.
 +
units = [('mm', 'cm', 'm', 'in',),
 +
        ('Milimeters', 'Centimeters', 'Meters', 'Inches',),
 +
        (1000, 100, 1, 39.3701,)]
 +
 
 +
 
 +
#
 +
# Triangle soup class gets called with the contents of a surface. We collect
 +
# vertices for each segment and dump each polygon as a triangle.
 +
#
 +
 
 +
class TriSoup(lxifc.TriangleSoup):
 +
    def __init__(self):
 +
        self.fmtASCII = False
 +
        self.polycount = 0
 +
        # output will eventually hold either a list of lines to write to the output
 +
        # file (ASCII format) or a single string of packed binary values
 +
        # (binary format). The exact tyep (list or string) is set by the saver
 +
        # when it creates the triangle soup object.
 +
        self.output = None
 +
        # scale factor for the destination/target application. Also set by the
 +
        # saver class
 +
        self.factor = None
 +
        self.vdesc = lx.service.Tableau().AllocVertex()
 +
        self.vdesc.AddFeature(lx.symbol.iTBLX_BASEFEATURE, lx.symbol.sTBLX_FEATURE_POS)
 +
 
 +
    def Sample(self, surf):
 +
        surf.SetVertex(self.vdesc)
 +
        surf.Sample(surf.Bound(), -1.0, self)
 +
 
 +
    def soup_Segment(self, segID, type):
 +
        # preps the next triangle?
 +
        self.vrts = []
 +
        return type == lx.symbol.iTBLX_SEG_TRIANGLE
 +
 
 +
    def soup_Vertex(self, vbuf):
 +
        # Gets the next vertex, scales it using the scale factor for the output
 +
        # (preference) units and adds it to the current polygon
 +
        vbuf.setType('f')
 +
        vbuf.setSize(3)
 +
        x = lxu.vector.scale(vbuf.get(), self.factor)
 +
        x = (x[2], x[0], x[1])
 +
        self.vrts.append(x)
 +
        return len(self.vrts) - 1
 +
 
 +
    def soup_Polygon(self, v0, v1, v2):
 +
        # process the next polygon
 +
        x0 = lxu.vector.sub(self.vrts[v1], self.vrts[v0])
 +
        x1 = lxu.vector.sub(self.vrts[v2], self.vrts[v0])
 +
        norm = lxu.vector.normalize(lxu.vector.cross(x0, x1))
 +
 
 +
        # If the output format is ASCII add the next polygon to the list of strings
 +
        if self.fmtASCII:
 +
            self.output.append("facet normal {0:.8f} {1:.8f} {2:.8f}".format(norm[0], norm[1], norm[2]))
 +
            self.output.append("  outer loop")
 +
 
 +
            for v in (v0, v1, v2):
 +
                pos = self.vrts[v]
 +
                self.output.append("    vertex {0} {1} {2}".format(pos[0], pos[1], pos[2]))
 +
 
 +
            self.output.append("  end loop")
 +
            self.output.append("end facet")
 +
 
 +
        # otherwise we're outputting binary format so we pack the vert & normal
 +
        # values for the next polygon and add them to the output string.
 +
        else:
 +
            p0x, p0y, p0z = self.vrts[v0]
 +
            p1x, p1y, p1z = self.vrts[v1]
 +
            p2x, p2y, p2z = self.vrts[v2]
 +
            self.output += struct.pack('12fxx', norm[0], norm[1], norm[2], p0x, p0y, p0z, p1x, p1y, p1z, p2x, p2y, p2z)
 +
            self.polycount += 1
 +
 
 +
 
 +
 
 +
#
 +
# The main STL saver class
 +
#
 +
 
 +
class STLSaver(lxifc.Saver):
 +
 
 +
    # Saving gathers all the surface items in the scene, and then scans
 +
    # them as if they were a single triangle soup. The result of the
 +
    # scan is lines of text which we output to the file at the end.
 +
    def sav_Save(self, source, filename, monitor):
 +
 
 +
        # get the output unit and format from preferences - see the 'CmdSTLExportSettings'
 +
        # custom command further down for details.
 +
        cmd_svc = lx.service.Command()
 +
        cmd = cmd_svc.Spawn(lx.symbol.iCTAG_NULL, 'stl.settings')
 +
        # Are we saving as ASCII or binary?
 +
        va, idx = cmd_svc.QueryArgString(cmd, 0, 'stl.settings format:?', 1)
 +
        fmtASCII = va.GetInt(0)
 +
        # what units does the destination application use?
 +
        va, idx = cmd_svc.QueryArgString(cmd, 0, 'stl.settings units:?', 1)
 +
        unit = va.GetString(0)
 +
        # get the scale factor for the destination application according to units.
 +
        scale_factor = units[2][units[0].index(unit)]
 +
 
 +
        # Get the current scene
 +
        scene = lx.object.Scene(source)
 +
        # and a channel read object
 +
        cread = scene.Channels(None, lx.service.Selection().GetTime())
 +
 
 +
        # Get list of Surface interfaces in the scene.
 +
        silst = []
 +
        isurf = lx.object.SurfaceItem()
 +
 
 +
        for i in range(scene.ItemCount(-1)):
 +
            item = scene.ItemByIndex(-1, i)
 +
            if isurf.set(item):
 +
                silst.append(isurf.GetSurface(cread, 1))
 +
 
 +
        soup = TriSoup()
 +
        soup.fmtASCII = fmtASCII
 +
        soup.factor = scale_factor
 +
        # If the ouput format is ASCII the triangle soup object needs a list to
 +
        # populate with lines for the ouput file, otherwise it needs an empty string
 +
        if fmtASCII:
 +
            soup.output = []
 +
        else:
 +
            soup.output = ''
 +
        samp = lx.object.TableauSurface()
 +
 
 +
        for si in silst:
 +
            for i in range(si.BinCount()):
 +
                samp.set(si.BinByIndex(i))
 +
                soup.Sample(samp)
 +
 
 +
        # If the ouput format is ASCII write the file to disk usng the triangle
 +
        # soup output list
 +
        if fmtASCII:
 +
            file = open(filename, 'w')
 +
            file.write('solid\n')
 +
            file.writelines( [x + '\n' for x in soup.output] )
 +
            file.write('end solid\n')
 +
            file.close()
 +
 
 +
        # otherwise output is binary and we just need to pack the required header
 +
        # and poly count value then dump that followed by the triangle soup's
 +
        # 'output' string.
 +
        else:
 +
            file = open(filename, 'wb')
 +
            file.write(struct.pack('80sl', 'Output by Luxology\'s modo', soup.polycount))
 +
            file.write(soup.output)
 +
            file.close()
 +
 
 +
# Bless the class to make it a first-class saver.
 +
tags = {
 +
    lx.symbol.sSRV_USERNAME: "Stereolithograhpy STL",
 +
    lx.symbol.sSAV_OUTCLASS:  lx.symbol.a_SCENE,
 +
    lx.symbol.sSAV_DOSTYPE : "STL"
 +
}
 +
lx.bless(STLSaver, "pySTLScene2", tags)
 +
 
 +
 
 +
 
 +
 
 +
#===============================================================================
 +
# Custom command to set STL export prefs. Implements persistent storage to store
 +
# the preference settings in the user config file.
 +
#===============================================================================
 +
 
 +
#
 +
# UIValueHints class deining a pop-up choice for the target application's default
 +
# units.
 +
#
 +
 
 +
class UnitsPopup(lxifc.UIValueHints):
 +
    def __init__(self, list):
 +
        self._list = list
 +
 
 +
    def uiv_Flags(self):
 +
        # This can be a series of flags, but in this case we're only returning
 +
        # 'fVALHINT_POPUPS' to indicate that we just need a straight pop-up
 +
        # List implemented.
 +
        return lx.symbol.fVALHINT_POPUPS
 +
 
 +
    def uiv_PopCount(self):
 +
        # returns the number of items in the list/pop-up
 +
        return len(self._list[0])
 +
 
 +
    def uiv_PopUserName(self,index):
 +
        # returns the Username of the item at 'index'
 +
        return self._list[1][index]
 +
 
 +
    def uiv_PopInternalName(self,index):
 +
        # returns the internal name of the item at 'index' - this will be the
 +
        # value returned when the custom command is queried
 +
        return self._list[0][index]
 +
 
 +
 
 +
#
 +
# Persistent data class - persistent date enables storing and retrieval of
 +
# attribute values (or any other data) as entries in the user config file.
 +
#
 +
# NOTE: usually you'd probably only define a complete class for
 +
# the persistent data if you needed to store a reasonably complex collection of
 +
# attribute values. For an example of a much simpler implementation see example 1
 +
# at: http://sdk.luxology.com/wiki/Persistent_Data
 +
#
 +
# In this particular example we have just two values that we need to store, a
 +
# string value that specifies the units that the destination application will use
 +
# to interpret the (dimensionless) values stored in the output file and a boolean
 +
# value that specifies whether the output format is ASCII or binary.
 +
#
 +
class STLPersistData(object):
 +
    def __init__(self):
 +
        # 'accesor' object for the 'units' atom
 +
        self.units = None
 +
        # 'accesor' object for the 'format' atom
 +
        self.format = None
 +
 
 +
        # the "units" atom's actual value - set to "cm" by default. This is
 +
        # actually an "attribute" object connected to the "units" accessor
 +
        # defined above.
 +
        self.units_val = 'cm'
 +
        # the "format" atom's actual value - set to "False" by default
 +
        self.format_val = False
 +
 
 +
    def get_units(self):
 +
        # returns the value of the 'units' atom or a default value of 'cm'
 +
        # if the read fails (eg if 'units_val' is unset for any reason)
 +
        try:
 +
            return self.units_val.GetString(0)
 +
        except:
 +
            return 'cm'
 +
 
 +
    def get_format(self):
 +
        # returns the value of the 'format' atom or a default value of 'False' (0)
 +
        # if the read fails (eg if 'format_val' is unset for any reason)
 +
        try:
 +
            return self.format_val.GetInt(0)
 +
        except:
 +
            return 0
 +
 
 +
    def set_units(self, units):
 +
        # appends a 'units' atom and writes the current value of the 'units_val'
 +
        # attribute
 +
        self.units.Append()
 +
        self.units_val.SetString(0, units)
 +
 
 +
    def set_format(self, format):
 +
        # appends a 'format' atom and writes the current value of the 'format_val'
 +
        # attribute
 +
        self.format.Append()
 +
        self.format_val.SetInt(0, format)
 +
 
 +
 
 +
# We need to store a global reference to the persistent data instance so that it can
 +
# be accessed throughout an entire session.
 +
pd = None
 +
 
 +
 
 +
#
 +
# This is the persistent data visitor class. It's is responsible for walking
 +
# the XML structure inside the top level atom - the top level (outer countainer)
 +
# atom, in this example the 'STLSaverSettings' atom, is created when our '
 +
# PersistData' instance is configured (see 'persist_setup()' function below).
 +
# This will result in a config entry like the following if the settings are
 +
# changed from their defaults.
 +
#
 +
# <atom type="STLSaverSettings">
 +
#    <atom type="units">mm</atom>
 +
#    <atom type="format">1</atom>
 +
# </atom>
 +
#
 +
class VisSTLSettings(lxifc.Visitor):
 +
    def vis_Evaluate(self):
 +
        # grab a reference to the session-wide global persistent data instance
 +
        global pd
 +
        persist_svc = lx.service.Persistence()
 +
 
 +
        # create 'units' atom
 +
        persist_svc.Start("units", lx.symbol.i_PERSIST_ATOM)
 +
        # add a string value
 +
        persist_svc.AddValue(lx.symbol.sTYPE_STRING)
 +
        # closing the atom returns the 'accessor' object which we assign
 +
        # to the variable set up in our persistent data object.
 +
        pd.units = persist_svc.End()
 +
        # attach the persistent data object's "units_val" variable as an
 +
        # attribute on the "units" accessor object.
 +
        pd.units_val = lx.object.Attributes(pd.units)
 +
 
 +
        # create & set the 'format' atom
 +
        persist_svc.Start("format", lx.symbol.i_PERSIST_ATOM)
 +
        persist_svc.AddValue(lx.symbol.sTYPE_BOOLEAN)
 +
        pd.format = persist_svc.End()
 +
        pd.format_val = lx.object.Attributes(pd.format)
 +
 
 +
        return lx.symbol.e_OK
 +
 
 +
 
 +
# persistent data setup function.
 +
def persist_setup():
 +
    # grab a reference to the session-wide persistent data instance
 +
    global pd
 +
    # IMPORTANT: check to see if it's not None, return if it isn't
 +
    # as we only want to configure it once per session!!!
 +
    if pd:
 +
        return
 +
    # create our persitent data object
 +
    pd = STLPersistData()
 +
    persist_svc = lx.service.Persistence()
 +
    # and our persistent data visitor
 +
    persist_vis = VisSTLSettings()
 +
    # configure the persistent data visitor - initialises the outer
 +
    # atom of the config entry.
 +
    persist_svc.Configure('STLSaverSettings', persist_vis)
 +
 
 +
 
 +
 
 +
#
 +
# This is the custom command that we're going to use to to read and write the
 +
# persistent data and to embed in a form (preferences) as a queriable command
 +
# for the saver to determine the user's current output settings.
 +
#
 +
class CmdSTLExportSettings(lxu.command.BasicCommand):
 +
    # grab a reference to the session-wide persistent data instance
 +
    global pd
 +
    def __init__(self):
 +
        lxu.command.BasicCommand.__init__(self)
 +
        persist_setup()
 +
 
 +
        # Add a string attribute for the "units" value
 +
        self.dyna_Add('units', lx.symbol.sTYPE_STRING)
 +
        # Add a boolean atttribute to store the format (ASCII or binary)
 +
        self.dyna_Add('format', lx.symbol.sTYPE_BOOLEAN)
 +
 
 +
        # set the flags for the attributes - both are queriable and both are optional
 +
        # ie they can be set and queried individually.
 +
        self.basic_SetFlags(0, lx.symbol.fCMDARG_QUERY | lx.symbol.fCMDARG_OPTIONAL)
 +
        self.basic_SetFlags(1, lx.symbol.fCMDARG_QUERY | lx.symbol.fCMDARG_OPTIONAL)
 +
 
 +
 
 +
    def arg_UIHints(self, index, hints):
 +
        # set the hints for the attributes' labels - the label that will appear
 +
        # next to the control on the form the command is embedded in.
 +
        if index == 0:
 +
            hints.Label("Units")
 +
        if index == 1:
 +
            hints.Label("ASCII Output")
 +
 
 +
 
 +
    def arg_UIValueHints(self, index):
 +
        # create an instance of our pop-up list object passing it the
 +
        # list of units (see list defined at top of file).
 +
        if index == 0:
 +
            return UnitsPopup(units)
 +
 
 +
 
 +
    def basic_Execute(self, msg, flags):
 +
        # execute is fired when the value of either of the attributes changes in
 +
        # the UI. We simply set the relevent attribute on the persistent data object.
 +
        if self.dyna_IsSet(0):
 +
            pd.set_units(self.dyna_String(0))
 +
        if self.dyna_IsSet(1):
 +
            pd.set_format(self.dyna_Bool(1))
 +
 
 +
 
 +
    def cmd_Query(self,index,vaQuery):
 +
        # query method reads the current value of the requested attribute from the
 +
        # persistent data object.
 +
        va = lx.object.ValueArray()
 +
        va.set(vaQuery)
 +
        if index == 0:
 +
            va.AddString(pd.get_units())
 +
        elif index == 1:
 +
            va.AddInt(pd.get_format())
 +
 
 +
        return lx.result.OK
 +
 
 +
 
 +
lx.bless(CmdSTLExportSettings, "stl.settings")
 +
 
 +
</syntaxhighlight>

Revision as of 22:45, 17 May 2013

Introduction

A walkthrough of the implementation of the STL scene saver plugin that ships with modo 701. Features covered include:

  • TriangleSoup class to parse the mesh surfaces and enumerate the polgons & points
  • Saver Server class
  • Use of Persistent Data and Visitor classes to store and retrieve data from the user config file.
  • Custom Command plugin and config files to expose the persistent data as settings in modo's preferences dialog

to be continued ....


# STL saver plug-in, in Python
 
import struct
import lx
import lxifc
import lxu.vector
import lxu.command
 
# these are the default units used by the target application to interpret the
# dimensionless values stored in the STL file along with a conversion value. The
# user will specify the unit as a prefernce setting and the scale value will be used
# scale the values output to the STL file so that the object appears the right
# size in the target application.
units = [('mm', 'cm', 'm', 'in',),
         ('Milimeters', 'Centimeters', 'Meters', 'Inches',),
         (1000, 100, 1, 39.3701,)]
 
 
#
# Triangle soup class gets called with the contents of a surface. We collect
# vertices for each segment and dump each polygon as a triangle.
#
 
class TriSoup(lxifc.TriangleSoup):
    def __init__(self):
        self.fmtASCII = False
        self.polycount = 0
        # output will eventually hold either a list of lines to write to the output
        # file (ASCII format) or a single string of packed binary values
        # (binary format). The exact tyep (list or string) is set by the saver
        # when it creates the triangle soup object.
        self.output = None
        # scale factor for the destination/target application. Also set by the
        # saver class
        self.factor = None
        self.vdesc = lx.service.Tableau().AllocVertex()
        self.vdesc.AddFeature(lx.symbol.iTBLX_BASEFEATURE, lx.symbol.sTBLX_FEATURE_POS)
 
    def Sample(self, surf):
        surf.SetVertex(self.vdesc)
        surf.Sample(surf.Bound(), -1.0, self)
 
    def soup_Segment(self, segID, type):
        # preps the next triangle?
        self.vrts = []
        return type == lx.symbol.iTBLX_SEG_TRIANGLE
 
    def soup_Vertex(self, vbuf):
        # Gets the next vertex, scales it using the scale factor for the output
        # (preference) units and adds it to the current polygon
        vbuf.setType('f')
        vbuf.setSize(3)
        x = lxu.vector.scale(vbuf.get(), self.factor)
        x = (x[2], x[0], x[1])
        self.vrts.append(x)
        return len(self.vrts) - 1
 
    def soup_Polygon(self, v0, v1, v2):
        # process the next polygon
        x0 = lxu.vector.sub(self.vrts[v1], self.vrts[v0])
        x1 = lxu.vector.sub(self.vrts[v2], self.vrts[v0])
        norm = lxu.vector.normalize(lxu.vector.cross(x0, x1))
 
        # If the output format is ASCII add the next polygon to the list of strings
        if self.fmtASCII:
            self.output.append("facet normal {0:.8f} {1:.8f} {2:.8f}".format(norm[0], norm[1], norm[2]))
            self.output.append("  outer loop")
 
            for v in (v0, v1, v2):
                pos = self.vrts[v]
                self.output.append("    vertex {0} {1} {2}".format(pos[0], pos[1], pos[2]))
 
            self.output.append("  end loop")
            self.output.append("end facet")
 
        # otherwise we're outputting binary format so we pack the vert & normal
        # values for the next polygon and add them to the output string.
        else:
            p0x, p0y, p0z = self.vrts[v0]
            p1x, p1y, p1z = self.vrts[v1]
            p2x, p2y, p2z = self.vrts[v2]
            self.output += struct.pack('12fxx', norm[0], norm[1], norm[2], p0x, p0y, p0z, p1x, p1y, p1z, p2x, p2y, p2z)
            self.polycount += 1
 
 
 
#
# The main STL saver class
#
 
class STLSaver(lxifc.Saver):
 
    # Saving gathers all the surface items in the scene, and then scans
    # them as if they were a single triangle soup. The result of the
    # scan is lines of text which we output to the file at the end.
    def sav_Save(self, source, filename, monitor):
 
        # get the output unit and format from preferences - see the 'CmdSTLExportSettings'
        # custom command further down for details.
        cmd_svc = lx.service.Command()
        cmd = cmd_svc.Spawn(lx.symbol.iCTAG_NULL, 'stl.settings')
        # Are we saving as ASCII or binary?
        va, idx = cmd_svc.QueryArgString(cmd, 0, 'stl.settings format:?', 1)
        fmtASCII = va.GetInt(0)
        # what units does the destination application use?
        va, idx = cmd_svc.QueryArgString(cmd, 0, 'stl.settings units:?', 1)
        unit = va.GetString(0)
        # get the scale factor for the destination application according to units.
        scale_factor = units[2][units[0].index(unit)]
 
        # Get the current scene
        scene = lx.object.Scene(source)
        # and a channel read object
        cread = scene.Channels(None, lx.service.Selection().GetTime())
 
        # Get list of Surface interfaces in the scene.
        silst = []
        isurf = lx.object.SurfaceItem()
 
        for i in range(scene.ItemCount(-1)):
            item = scene.ItemByIndex(-1, i)
            if isurf.set(item):
                silst.append(isurf.GetSurface(cread, 1))
 
        soup = TriSoup()
        soup.fmtASCII = fmtASCII
        soup.factor = scale_factor
        # If the ouput format is ASCII the triangle soup object needs a list to
        # populate with lines for the ouput file, otherwise it needs an empty string
        if fmtASCII:
            soup.output = []
        else:
            soup.output = ''
        samp = lx.object.TableauSurface()
 
        for si in silst:
            for i in range(si.BinCount()):
                samp.set(si.BinByIndex(i))
                soup.Sample(samp)
 
        # If the ouput format is ASCII write the file to disk usng the triangle
        # soup output list
        if fmtASCII:
            file = open(filename, 'w')
            file.write('solid\n')
            file.writelines( [x + '\n' for x in soup.output] )
            file.write('end solid\n')
            file.close()
 
        # otherwise output is binary and we just need to pack the required header
        # and poly count value then dump that followed by the triangle soup's
        # 'output' string.
        else:
            file = open(filename, 'wb')
            file.write(struct.pack('80sl', 'Output by Luxology\'s modo', soup.polycount))
            file.write(soup.output)
            file.close()
 
# Bless the class to make it a first-class saver.
tags = {
    lx.symbol.sSRV_USERNAME: "Stereolithograhpy STL",
    lx.symbol.sSAV_OUTCLASS:  lx.symbol.a_SCENE,
    lx.symbol.sSAV_DOSTYPE : "STL"
}
lx.bless(STLSaver, "pySTLScene2", tags)
 
 
 
 
#===============================================================================
# Custom command to set STL export prefs. Implements persistent storage to store
# the preference settings in the user config file.
#===============================================================================
 
#
# UIValueHints class deining a pop-up choice for the target application's default
# units.
#
 
class UnitsPopup(lxifc.UIValueHints):
    def __init__(self, list):
        self._list = list
 
    def uiv_Flags(self):
        # This can be a series of flags, but in this case we're only returning
        # 'fVALHINT_POPUPS' to indicate that we just need a straight pop-up
        # List implemented.
        return lx.symbol.fVALHINT_POPUPS
 
    def uiv_PopCount(self):
        # returns the number of items in the list/pop-up
        return len(self._list[0])
 
    def uiv_PopUserName(self,index):
        # returns the Username of the item at 'index'
        return self._list[1][index]
 
    def uiv_PopInternalName(self,index):
        # returns the internal name of the item at 'index' - this will be the
        # value returned when the custom command is queried
        return self._list[0][index]
 
 
#
# Persistent data class - persistent date enables storing and retrieval of
# attribute values (or any other data) as entries in the user config file.
#
# NOTE: usually you'd probably only define a complete class for
# the persistent data if you needed to store a reasonably complex collection of
# attribute values. For an example of a much simpler implementation see example 1
# at: http://sdk.luxology.com/wiki/Persistent_Data
#
# In this particular example we have just two values that we need to store, a
# string value that specifies the units that the destination application will use
# to interpret the (dimensionless) values stored in the output file and a boolean
# value that specifies whether the output format is ASCII or binary.
#
class STLPersistData(object):
    def __init__(self):
        # 'accesor' object for the 'units' atom
        self.units = None
        # 'accesor' object for the 'format' atom
        self.format = None
 
        # the "units" atom's actual value - set to "cm" by default. This is
        # actually an "attribute" object connected to the "units" accessor
        # defined above.
        self.units_val = 'cm'
        # the "format" atom's actual value - set to "False" by default
        self.format_val = False
 
    def get_units(self):
        # returns the value of the 'units' atom or a default value of 'cm'
        # if the read fails (eg if 'units_val' is unset for any reason)
        try:
            return self.units_val.GetString(0)
        except:
            return 'cm'
 
    def get_format(self):
        # returns the value of the 'format' atom or a default value of 'False' (0)
        # if the read fails (eg if 'format_val' is unset for any reason)
        try:
            return self.format_val.GetInt(0)
        except:
            return 0
 
    def set_units(self, units):
        # appends a 'units' atom and writes the current value of the 'units_val'
        # attribute
        self.units.Append()
        self.units_val.SetString(0, units)
 
    def set_format(self, format):
        # appends a 'format' atom and writes the current value of the 'format_val'
        # attribute
        self.format.Append()
        self.format_val.SetInt(0, format)
 
 
# We need to store a global reference to the persistent data instance so that it can
# be accessed throughout an entire session.
pd = None
 
 
#
# This is the persistent data visitor class. It's is responsible for walking
# the XML structure inside the top level atom - the top level (outer countainer)
# atom, in this example the 'STLSaverSettings' atom, is created when our '
# PersistData' instance is configured (see 'persist_setup()' function below).
# This will result in a config entry like the following if the settings are
# changed from their defaults.
#
# <atom type="STLSaverSettings">
#    <atom type="units">mm</atom>
#    <atom type="format">1</atom>
# </atom>
#
class VisSTLSettings(lxifc.Visitor):
    def vis_Evaluate(self):
        # grab a reference to the session-wide global persistent data instance
        global pd
        persist_svc = lx.service.Persistence()
 
        # create 'units' atom
        persist_svc.Start("units", lx.symbol.i_PERSIST_ATOM)
        # add a string value
        persist_svc.AddValue(lx.symbol.sTYPE_STRING)
        # closing the atom returns the 'accessor' object which we assign
        # to the variable set up in our persistent data object.
        pd.units = persist_svc.End()
        # attach the persistent data object's "units_val" variable as an
        # attribute on the "units" accessor object.
        pd.units_val = lx.object.Attributes(pd.units)
 
        # create & set the 'format' atom
        persist_svc.Start("format", lx.symbol.i_PERSIST_ATOM)
        persist_svc.AddValue(lx.symbol.sTYPE_BOOLEAN)
        pd.format = persist_svc.End()
        pd.format_val = lx.object.Attributes(pd.format)
 
        return lx.symbol.e_OK
 
 
# persistent data setup function.
def persist_setup():
    # grab a reference to the session-wide persistent data instance
    global pd
    # IMPORTANT: check to see if it's not None, return if it isn't
    # as we only want to configure it once per session!!!
    if pd:
        return
    # create our persitent data object
    pd = STLPersistData()
    persist_svc = lx.service.Persistence()
    # and our persistent data visitor
    persist_vis = VisSTLSettings()
    # configure the persistent data visitor - initialises the outer
    # atom of the config entry.
    persist_svc.Configure('STLSaverSettings', persist_vis)
 
 
 
#
# This is the custom command that we're going to use to to read and write the
# persistent data and to embed in a form (preferences) as a queriable command
# for the saver to determine the user's current output settings.
#
class CmdSTLExportSettings(lxu.command.BasicCommand):
    # grab a reference to the session-wide persistent data instance
    global pd
    def __init__(self):
        lxu.command.BasicCommand.__init__(self)
        persist_setup()
 
        # Add a string attribute for the "units" value
        self.dyna_Add('units', lx.symbol.sTYPE_STRING)
        # Add a boolean atttribute to store the format (ASCII or binary)
        self.dyna_Add('format', lx.symbol.sTYPE_BOOLEAN)
 
        # set the flags for the attributes - both are queriable and both are optional
        # ie they can be set and queried individually.
        self.basic_SetFlags(0, lx.symbol.fCMDARG_QUERY | lx.symbol.fCMDARG_OPTIONAL)
        self.basic_SetFlags(1, lx.symbol.fCMDARG_QUERY | lx.symbol.fCMDARG_OPTIONAL)
 
 
    def arg_UIHints(self, index, hints):
        # set the hints for the attributes' labels - the label that will appear
        # next to the control on the form the command is embedded in.
        if index == 0:
            hints.Label("Units")
        if index == 1:
            hints.Label("ASCII Output")
 
 
    def arg_UIValueHints(self, index):
        # create an instance of our pop-up list object passing it the
        # list of units (see list defined at top of file).
        if index == 0:
            return UnitsPopup(units)
 
 
    def basic_Execute(self, msg, flags):
        # execute is fired when the value of either of the attributes changes in
        # the UI. We simply set the relevent attribute on the persistent data object.
        if self.dyna_IsSet(0):
            pd.set_units(self.dyna_String(0))
        if self.dyna_IsSet(1):
            pd.set_format(self.dyna_Bool(1))
 
 
    def cmd_Query(self,index,vaQuery):
        # query method reads the current value of the requested attribute from the
        # persistent data object.
        va = lx.object.ValueArray()
        va.set(vaQuery)
        if index == 0:
            va.AddString(pd.get_units())
        elif index == 1:
            va.AddInt(pd.get_format())
 
        return lx.result.OK
 
 
lx.bless(CmdSTLExportSettings, "stl.settings")