Scene Saver: STL

From The Foundry MODO SDK wiki
Revision as of 11:37, 18 May 2013 by GwynneR (Talk | contribs) (Introduction)

Jump to: navigation, search


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

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

The Scene Saver and Custom Command Plugin Code

#!/usr/bin/env python
# STL saver plug-in, in Python
import struct
import lx
import lxifc
import lxu.vector
import lxu.command
# "units" is a list of the default units used by the target application to
# interpret the values stored in the STL file and which will be used by the
# export settings custom command defined later to populate the "units" pop-up
# list choice.
# The list contains three tuples, the internal names used by the pop-up class,
# the "Usernames" used by the pop-up class and a "scale factor" per unit that
# will be used scale the vertex values output to the STL file in order to match
# the units used by the destination application to interpret the STL file.
units = [('mm', 'cm', 'm', 'in',),
         ('Milimeters', 'Centimeters', 'Meters', 'Inches',),
         (1000, 100, 1, 39.3701,)]
# The TriSoup (TriangleSoup) class gets called by the "Saver" class 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 type (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):
        # clear the verts list and return a new triangle segment.
        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
# The saving process gathers all the surface items in the scene, and scans them
# as if they were a single triangle soup. The result of the scan is either lines
# of text (for ASCII output) or a packed string of binary values (for binary
# output) which we write to the destination file at the end.
class STLSaver(lxifc.Saver):
    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 items 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))
        # create & initialise the TriangleSoup object
        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()
        # parse each surface item in turn & build a list of triangles to output.
        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))
# Set the server's tags
tags = {
    # The "UserName" is the string that appears in the file save dialog's pop-up
    # list of savers.
    lx.symbol.sSRV_USERNAME: "Stereolithograhpy STL",
    # declares the plugin as a "Saver" of type "SCENE"
    lx.symbol.sSAV_OUTCLASS:  lx.symbol.a_SCENE,
    # sets the DOS extension.
    lx.symbol.sSAV_DOSTYPE : "STL"
# bless the saver to register it as a first class server (plugin)
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 defining 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 or varied
# 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 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 declare/store a global reference to the persistent data object so
# that it can be accessed throughout an entire session. Here we just set it to
# "None", it will be assigned/configured as an actual persistent data object
# when the export settings custom command is first blessed (registered as a
# plugin server)
persist_data = 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 persist_data
        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.
        persist_data.units = persist_svc.End()
        # attach the persistent data object's "units_val" variable as an
        # attribute on the "units" accessor object.
        persist_data.units_val = lx.object.Attributes(persist_data.units)
        # create & set the 'format' atom
        persist_svc.Start("format", lx.symbol.i_PERSIST_ATOM)
        persist_data.format = persist_svc.End()
        persist_data.format_val = lx.object.Attributes(persist_data.format)
        return lx.symbol.e_OK
# persistent data setup function. Called by the custom command below to
# configure the persistent data object.
# IMPORTANT NOTE: configuration should only be performed ONCE per session, so
# the setup function should first check to determine whether the persist_data
# object exists before continuing.
def persist_setup():
    # grab a reference to the session-wide persistent data instance
    global persist_data
    # 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 persist_data:
    # create our persitent data object
    persist_data = 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)
# And, finally, the custom command that we're going to use to to read and write
# the persistent data. The command can be embedded in a form, in this case a
# form in modo's preferences dialog, enabling users to set the default output
# properties of the saver (format and units) and queried by the 'Saver' class to
# retrieve the currently set preferences.
class CmdSTLExportSettings(lxu.command.BasicCommand):
    # grab a reference to the session-wide persistent data instance
    global persist_data
    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
# bless the command to register it as a first class server (plugin)
lx.bless(CmdSTLExportSettings, "stl.settings")

The Complete Config File

<?xml version="1.0" encoding="UTF-8"?>
  <atom type="Attributes">
    <hash type="Sheet" key="05044994227:sheet">
      <atom type="Label">STL Object Export</atom>
      <list type="Control" val="cmd stl.settings units:?">
        <atom type="Label">Interpret units as</atom>
        <atom type="Tooltip">Set the units that the destination software will use to interpret the values in the STL file</atom>
      <list type="Control" val="cmd stl.settings format:?">
        <atom type="Label">Save as ASCII</atom>
        <atom type="Tooltip">Save the STL file as ASCII, default is binary output.</atom>
      <atom type="Filter">prefs/fileio/stlio:filterPreset</atom>
      <hash type="InCategory" key="prefs:general#head">
        <atom type="Ordinal">80.8</atom>
      <atom type="Group">prefs/fileio</atom>
  <atom type="Filters">
    <hash type="Preset" key="prefs/fileio/stlio:filterPreset">
      <atom type="Name">STL I/O</atom>
      <!-- 20385740002:filterCat is the "Preferences" category -->
      <atom type="Category">20385740002:filterCat</atom>
      <atom type="Enable">1</atom>
      <list type="Node">1 .group 0 &quot;&quot;</list>
      <list type="Node">1 prefType fileio/stlio</list>
      <list type="Node">-1 .endgroup </list>
  <atom type="PreferenceCategories">
    <hash type="PrefCat" key="fileio/stlio"></hash>
  <atom type="Messages">
    <hash type="Table" key="preferences.categories.en_US">
      <hash type="T" key="fileio/stlio">STL I/O</hash>
  <atom type="CommandHelp">
    <hash type="Command" key="stl.settings@en_US">
      <atom type="UserName">STL Saver settings</atom>
      <atom type="ButtonName">STL Prefs</atom>
      <atom type="Desc">Command to set the output prefernces of the STL saver</atom>
      <atom type="ToolTip">Set the STL saver preferences</atom>
      <hash type="Argument" key="units">
        <atom type="UserName">Units</atom>
        <atom type="Desc">Selects which units the destination application will use.</atom>
        <atom type="ToolTip">Selects which units the destination application will use to interpret the values in the STL file.</atom>
      <hash type="Argument" key="format">
        <atom type="UserName">Save ASCII</atom>
        <atom type="Desc">Save the file in ASCII format, default is binary.</atom>
        <atom type="ToolTip">Save the file in ASCII format, default is binary output.</atom>