Scene Saver: STL

From The Foundry MODO SDK wiki
Revision as of 22:45, 17 May 2013 by GwynneR (Talk | contribs)

Jump to: navigation, search


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.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
        x = lxu.vector.scale(vbuf.get(), self.factor)
        x = (x[2], x[0], x[1])
        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.
            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 = []
            soup.output = ''
        samp = lx.object.TableauSurface()
        for si in silst:
            for i in range(si.BinCount()):
        # If the ouput format is ASCII write the file to disk usng the triangle
        # soup output list
        if fmtASCII:
            file = open(filename, 'w')
            file.writelines( [x + '\n' for x in soup.output] )
            file.write('end solid\n')
        # 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.
            file = open(filename, 'wb')
            file.write(struct.pack('80sl', 'Output by Luxology\'s modo', soup.polycount))
# 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:
# 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)
            return self.units_val.GetString(0)
            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)
            return self.format_val.GetInt(0)
            return 0
    def set_units(self, units):
        # appends a 'units' atom and writes the current value of the 'units_val'
        # attribute
        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_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
        # 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)
        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:
    # 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):
        # 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:
        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):
        if self.dyna_IsSet(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()
        if index == 0:
        elif index == 1:
        return lx.result.OK
lx.bless(CmdSTLExportSettings, "stl.settings")