Difference between revisions of "Scene Saver: STL"

From The Foundry MODO SDK wiki
Jump to: navigation, search
(The Complete Config File)
 
(33 intermediate revisions by the same user not shown)
Line 3: Line 3:
 
A walkthrough of the implementation of the STL scene saver plugin that ships with modo 701. Features covered include:
 
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
+
* Using a [[LXu_TRIANGLESOUP_(index)#C16|TriangleSoup]] object to parse the mesh surfaces and enumerate the polgons & points
* [[Saver:_Server_basics|Saver Server]] class
+
* Scene [[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.
 
* 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
+
* Custom Command plugin and associated configuration files to expose the persistent data as settings in modo's preferences dialog
  
to be continued ....
+
<br /><br />
  
== The Full Plugin Code ==
+
== The Scene Saver and Custom Command Plugin Code ==
 
<syntaxhighlight lang="python">
 
<syntaxhighlight lang="python">
# STL saver plug-in, in Python
+
#!/usr/bin/env python
  
import struct
+
# STL saver plug-in, in Python
 +
import StringIO
 +
from struct import Struct, pack
 
import lx
 
import lx
 
import lxifc
 
import lxifc
Line 20: Line 22:
 
import lxu.command
 
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
+
# "units" is a list of the default units used by the target application to
# size in the target application.
+
# 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',),
 
units = [('mm', 'cm', 'm', 'in',),
 
         ('Milimeters', 'Centimeters', 'Meters', 'Inches',),
 
         ('Milimeters', 'Centimeters', 'Meters', 'Inches',),
Line 30: Line 41:
  
  
#
+
 
# Triangle soup class gets called with the contents of a surface. We collect
+
#===============================================================================
# vertices for each segment and dump each polygon as a triangle.
+
# 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):
 
class TriSoup(lxifc.TriangleSoup):
Line 41: Line 54:
 
         # output will eventually hold either a list of lines to write to the output
 
         # 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
 
         # file (ASCII format) or a single string of packed binary values
         # (binary format). The exact tyep (list or string) is set by the saver
+
         # (binary format). The exact type (list or string) is set by the saver
 
         # when it creates the triangle soup object.
 
         # when it creates the triangle soup object.
 
         self.output = None
 
         self.output = None
 +
        # if we're outputting binary 's' will point to the pre-initialized 'Struct'
 +
        # object we're using to pack the binary data - set by the saver class when
 +
        # it creates the TriSoup object.
 +
        self.s = None
 +
 
         # scale factor for the destination/target application. Also set by the
 
         # scale factor for the destination/target application. Also set by the
 
         # saver class
 
         # saver class
Line 54: Line 72:
 
         surf.Sample(surf.Bound(), -1.0, self)
 
         surf.Sample(surf.Bound(), -1.0, self)
  
     def soup_Segment(self, segID, type):
+
     def soup_Segment(self, segID, stype):
         # preps the next triangle?
+
         # clear the verts list and return a new triangle segment.
 
         self.vrts = []
 
         self.vrts = []
         return type == lx.symbol.iTBLX_SEG_TRIANGLE
+
         return stype == lx.symbol.iTBLX_SEG_TRIANGLE
  
 
     def soup_Vertex(self, vbuf):
 
     def soup_Vertex(self, vbuf):
Line 93: Line 111:
 
             p1x, p1y, p1z = self.vrts[v1]
 
             p1x, p1y, p1z = self.vrts[v1]
 
             p2x, p2y, p2z = self.vrts[v2]
 
             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)
+
            # we're using the StringIO module so we can write to the output
 +
            # string as if it was a file
 +
             self.output.write(self.s.pack(norm[0], norm[1], norm[2], p0x, p0y, p0z, p1x, p1y, p1z, p2x, p2y, p2z))
 
             self.polycount += 1
 
             self.polycount += 1
  
  
  
#
+
#===============================================================================
 
# The main STL saver class
 
# 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):
 
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):
 
     def sav_Save(self, source, filename, monitor):
 
+
         # get the output unit and format from preferences - see the
         # get the output unit and format from preferences - see the 'CmdSTLExportSettings'
+
        # 'CmdSTLExportSettings' custom command further down for details.
        # custom command further down for details.
+
 
         cmd_svc = lx.service.Command()
 
         cmd_svc = lx.service.Command()
 
         cmd = cmd_svc.Spawn(lx.symbol.iCTAG_NULL, 'stl.settings')
 
         cmd = cmd_svc.Spawn(lx.symbol.iCTAG_NULL, 'stl.settings')
Line 127: Line 147:
 
         cread = scene.Channels(None, lx.service.Selection().GetTime())
 
         cread = scene.Channels(None, lx.service.Selection().GetTime())
  
         # Get list of Surface interfaces in the scene.
+
         # Get list of Surface items in the scene.
 
         silst = []
 
         silst = []
 
         isurf = lx.object.SurfaceItem()
 
         isurf = lx.object.SurfaceItem()
Line 136: Line 156:
 
                 silst.append(isurf.GetSurface(cread, 1))
 
                 silst.append(isurf.GetSurface(cread, 1))
  
 +
        # create & initialise the TriangleSoup object
 
         soup = TriSoup()
 
         soup = TriSoup()
 
         soup.fmtASCII = fmtASCII
 
         soup.fmtASCII = fmtASCII
 
         soup.factor = scale_factor
 
         soup.factor = scale_factor
 +
 
         # If the ouput format is ASCII the triangle soup object needs a list to
 
         # 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
+
         # populate with lines for the ouput file, otherwise it needs an empty
 +
        # string and we'll use the StringIO module instead of a raw string so
 +
        # that we can write to it like a file (improves performance)
 
         if fmtASCII:
 
         if fmtASCII:
 
             soup.output = []
 
             soup.output = []
 
         else:
 
         else:
             soup.output = ''
+
             soup.output = StringIO.StringIO()
 +
            soup.s = Struct('12fxx')
 
         samp = lx.object.TableauSurface()
 
         samp = lx.object.TableauSurface()
  
 +
        # parse each surface item in turn & build a list of triangles to output.
 
         for si in silst:
 
         for si in silst:
 
             for i in range(si.BinCount()):
 
             for i in range(si.BinCount()):
Line 155: Line 181:
 
         # soup output list
 
         # soup output list
 
         if fmtASCII:
 
         if fmtASCII:
             file = open(filename, 'w')
+
             try:
             file.write('solid\n')
+
                fout = open(filename, 'w')
            file.writelines( [x + '\n' for x in soup.output] )
+
             except:
            file.write('end solid\n')
+
                lx.throw(lx.result.FAILED)
            file.close()
+
            else:
 +
                fout.write('solid\n')
 +
                fout.writelines( [x + '\n' for x in soup.output] )
 +
                fout.write('end solid\n')
 +
                fout.close()
  
 
         # otherwise output is binary and we just need to pack the required header
 
         # otherwise output is binary and we just need to pack the required header
Line 165: Line 195:
 
         # 'output' string.
 
         # 'output' string.
 
         else:
 
         else:
             file = open(filename, 'wb')
+
             try:
             file.write(struct.pack('80sl', 'Output by Luxology\'s modo', soup.polycount))
+
                fout = open(filename, 'wb')
            file.write(soup.output)
+
             except:
            file.close()
+
                lx.throw(lx.result.FAILED)
 +
            else:
 +
                fout.write(pack('80sl', 'Output by Luxology\'s modo', soup.polycount))
 +
                fout.write(soup.output.getvalue())
 +
                fout.close()
  
# Bless the class to make it a first-class saver.
+
# Set the server's tags
 
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",
 
     lx.symbol.sSRV_USERNAME: "Stereolithograhpy STL",
 +
    # declares the plugin as a "Saver" of type "SCENE"
 
     lx.symbol.sSAV_OUTCLASS:  lx.symbol.a_SCENE,
 
     lx.symbol.sSAV_OUTCLASS:  lx.symbol.a_SCENE,
 +
    # sets the DOS extension.
 
     lx.symbol.sSAV_DOSTYPE : "STL"
 
     lx.symbol.sSAV_DOSTYPE : "STL"
 
}
 
}
 +
# bless the saver to register it as a first class server (plugin)
 
lx.bless(STLSaver, "pySTLScene2", tags)
 
lx.bless(STLSaver, "pySTLScene2", tags)
  
Line 186: Line 225:
 
#===============================================================================
 
#===============================================================================
  
#
+
 
# UIValueHints class deining a pop-up choice for the target application's default
+
#===============================================================================
# units.
+
# UIValueHints class defining a pop-up choice for the target application's
#
+
# default units.
 +
#===============================================================================
  
 
class UnitsPopup(lxifc.UIValueHints):
 
class UnitsPopup(lxifc.UIValueHints):
     def __init__(self, list):
+
     def __init__(self, items):
         self._list = list
+
         self._items = items
  
 
     def uiv_Flags(self):
 
     def uiv_Flags(self):
Line 203: Line 243:
 
     def uiv_PopCount(self):
 
     def uiv_PopCount(self):
 
         # returns the number of items in the list/pop-up
 
         # returns the number of items in the list/pop-up
         return len(self._list[0])
+
         return len(self._items[0])
  
 
     def uiv_PopUserName(self,index):
 
     def uiv_PopUserName(self,index):
 
         # returns the Username of the item at 'index'
 
         # returns the Username of the item at 'index'
         return self._list[1][index]
+
         return self._items[1][index]
  
 
     def uiv_PopInternalName(self,index):
 
     def uiv_PopInternalName(self,index):
 
         # returns the internal name of the item at 'index' - this will be the
 
         # returns the internal name of the item at 'index' - this will be the
 
         # value returned when the custom command is queried
 
         # value returned when the custom command is queried
         return self._list[0][index]
+
         return self._items[0][index]
  
  
#
+
 
 +
#===============================================================================
 
# Persistent data class - persistent date enables storing and retrieval of
 
# Persistent data class - persistent date enables storing and retrieval of
 
# attribute values (or any other data) as entries in the user config file.
 
# attribute values (or any other data) as entries in the user config file.
 
#
 
#
 
# NOTE: usually you'd probably only define a complete class for
 
# NOTE: usually you'd probably only define a complete class for
# the persistent data if you needed to store a reasonably complex collection of
+
# the persistent data if you needed to store a reasonably complex or varied
# attribute values. For an example of a much simpler implementation see example 1
+
# collection of attribute values. For an example of a much simpler implementation
# at: http://sdk.luxology.com/wiki/Persistent_Data
+
# see example 1 at: http://modo.sdk.thefoundry.co.uk/wiki/Persistent_Data
 
#
 
#
 
# In this particular example we have just two values that we need to store, a
 
# 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
+
# string value that specifies the units the destination application will use
 
# to interpret the (dimensionless) values stored in the output file and a boolean
 
# to interpret the (dimensionless) values stored in the output file and a boolean
 
# value that specifies whether the output format is ASCII or binary.
 
# value that specifies whether the output format is ASCII or binary.
#
+
#===============================================================================
 +
 
 
class STLPersistData(object):
 
class STLPersistData(object):
 
     def __init__(self):
 
     def __init__(self):
Line 234: Line 276:
 
         self.units = None
 
         self.units = None
 
         # 'accesor' object for the 'format' atom
 
         # 'accesor' object for the 'format' atom
         self.format = None
+
         self.saveASCII = None
  
 
         # the "units" atom's actual value - set to "cm" by default. This is
 
         # the "units" atom's actual value - set to "cm" by default. This is
Line 241: Line 283:
 
         self.units_val = 'cm'
 
         self.units_val = 'cm'
 
         # the "format" atom's actual value - set to "False" by default
 
         # the "format" atom's actual value - set to "False" by default
         self.format_val = False
+
         self.saveASCII_val = False
  
 
     def get_units(self):
 
     def get_units(self):
Line 253: Line 295:
 
     def get_format(self):
 
     def get_format(self):
 
         # returns the value of the 'format' atom or a default value of 'False' (0)
 
         # 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)
+
         # if the read fails (eg if 'saveASCII_val' is unset for any reason)
 
         try:
 
         try:
             return self.format_val.GetInt(0)
+
             return self.saveASCII_val.GetInt(0)
 
         except:
 
         except:
 
             return 0
 
             return 0
Line 265: Line 307:
 
         self.units_val.SetString(0, units)
 
         self.units_val.SetString(0, units)
  
     def set_format(self, format):
+
     def set_format(self, saveASCII):
         # appends a 'format' atom and writes the current value of the 'format_val'
+
         # appends a 'format' atom and writes the current value of the 'saveASCII_val'
 
         # attribute
 
         # attribute
         self.format.Append()
+
         self.saveASCII.Append()
         self.format_val.SetInt(0, format)
+
         self.saveASCII_val.SetInt(0, saveASCII)
  
  
# We need to store a global reference to the persistent data instance so that it can
 
# be accessed throughout an entire session.
 
pd = None
 
  
 +
#===============================================================================
 +
# 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
 
# 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)
 
# the XML structure inside the top level atom - the top level (outer countainer)
Line 289: Line 338:
 
#    <atom type="format">1</atom>
 
#    <atom type="format">1</atom>
 
# </atom>
 
# </atom>
#
+
#===============================================================================
 +
 
 
class VisSTLSettings(lxifc.Visitor):
 
class VisSTLSettings(lxifc.Visitor):
 
     def vis_Evaluate(self):
 
     def vis_Evaluate(self):
 
         # grab a reference to the session-wide global persistent data instance
 
         # grab a reference to the session-wide global persistent data instance
         global pd
+
         global persist_data
 
         persist_svc = lx.service.Persistence()
 
         persist_svc = lx.service.Persistence()
  
Line 302: Line 352:
 
         # closing the atom returns the 'accessor' object which we assign
 
         # closing the atom returns the 'accessor' object which we assign
 
         # to the variable set up in our persistent data object.
 
         # to the variable set up in our persistent data object.
         pd.units = persist_svc.End()
+
         persist_data.units = persist_svc.End()
 
         # attach the persistent data object's "units_val" variable as an
 
         # attach the persistent data object's "units_val" variable as an
 
         # attribute on the "units" accessor object.
 
         # attribute on the "units" accessor object.
         pd.units_val = lx.object.Attributes(pd.units)
+
         persist_data.units_val = lx.object.Attributes(persist_data.units)
  
 
         # create & set the 'format' atom
 
         # create & set the 'format' atom
 
         persist_svc.Start("format", lx.symbol.i_PERSIST_ATOM)
 
         persist_svc.Start("format", lx.symbol.i_PERSIST_ATOM)
 
         persist_svc.AddValue(lx.symbol.sTYPE_BOOLEAN)
 
         persist_svc.AddValue(lx.symbol.sTYPE_BOOLEAN)
         pd.format = persist_svc.End()
+
         persist_data.saveASCII = persist_svc.End()
         pd.format_val = lx.object.Attributes(pd.format)
+
         persist_data.saveASCII_val = lx.object.Attributes(persist_data.saveASCII)
  
 
         return lx.symbol.e_OK
 
         return lx.symbol.e_OK
  
  
# persistent data setup function.
+
 
 +
#===============================================================================
 +
# 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():
 
def persist_setup():
 
     # grab a reference to the session-wide persistent data instance
 
     # grab a reference to the session-wide persistent data instance
     global pd
+
     global persist_data
 
     # IMPORTANT: check to see if it's not None, return if it isn't
 
     # IMPORTANT: check to see if it's not None, return if it isn't
 
     # as we only want to configure it once per session!!!
 
     # as we only want to configure it once per session!!!
     if pd:
+
     if persist_data:
 
         return
 
         return
 
     # create our persitent data object
 
     # create our persitent data object
     pd = STLPersistData()
+
     persist_data = STLPersistData()
 
     persist_svc = lx.service.Persistence()
 
     persist_svc = lx.service.Persistence()
 
     # and our persistent data visitor
 
     # and our persistent data visitor
Line 335: Line 394:
  
  
#
+
#===============================================================================
# This is the custom command that we're going to use to to read and write the
+
# And, finally, the custom command that we're going to use to to read and write
# persistent data and to embed in a form (preferences) as a queriable command
+
# the persistent data. The command can be embedded in a form, in this case a
# for the saver to determine the user's current output settings.
+
# 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):
 
class CmdSTLExportSettings(lxu.command.BasicCommand):
 
     # grab a reference to the session-wide persistent data instance
 
     # grab a reference to the session-wide persistent data instance
     global pd
+
     global persist_data
 
     def __init__(self):
 
     def __init__(self):
 
         lxu.command.BasicCommand.__init__(self)
 
         lxu.command.BasicCommand.__init__(self)
Line 378: Line 440:
 
         # the UI. We simply set the relevent attribute on the persistent data object.
 
         # the UI. We simply set the relevent attribute on the persistent data object.
 
         if self.dyna_IsSet(0):
 
         if self.dyna_IsSet(0):
             pd.set_units(self.dyna_String(0))
+
             persist_data.set_units(self.dyna_String(0))
 
         if self.dyna_IsSet(1):
 
         if self.dyna_IsSet(1):
             pd.set_format(self.dyna_Bool(1))
+
             persist_data.set_format(self.dyna_Bool(1))
  
  
Line 389: Line 451:
 
         va.set(vaQuery)
 
         va.set(vaQuery)
 
         if index == 0:
 
         if index == 0:
             va.AddString(pd.get_units())
+
             va.AddString(persist_data.get_units())
 
         elif index == 1:
 
         elif index == 1:
             va.AddInt(pd.get_format())
+
             va.AddInt(persist_data.get_format())
 
+
 
         return lx.result.OK
 
         return lx.result.OK
 +
# bless the command to register it as a first class server (plugin)
 +
lx.bless(CmdSTLExportSettings, "stl.settings")
 +
</syntaxhighlight>
 +
<br /><br />
  
 +
== Building the Configuration Entries ==
 +
=== Adding to modo's Preferences dialog ===
 +
==== Creating a form ====
 +
We'll need a form to embed in modo's preferences dialog. It'll contain just two entries to query each of the STL saver's options defined by the stl.settings custom command. Two important items to note in the form definition are the InCategory hash which adds our form to preferences and the filter atom which allows the preferences form to dynamically place it in the correct section.
 +
<syntaxhighlight lang="xml">
 +
  <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>
 +
      <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>
 +
      </list>
 +
      <atom type="Filter">prefs/fileio/stlio:filterPreset</atom>
 +
      <hash type="InCategory" key="prefs:general#head">
 +
        <atom type="Ordinal">80.8</atom>
 +
      </hash>
 +
      <atom type="Group">prefs/fileio</atom>
 +
    </hash>
 +
  </atom>
 +
</syntaxhighlight>
 +
<br />
  
lx.bless(CmdSTLExportSettings, "stl.settings")
+
==== Adding a Filter Preset ====
 +
Define the filter preset  referenced by our form and which specifies where it should appear.
 +
<syntaxhighlight lang="xml">
 +
  <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>
 +
    </hash>
 +
  </atom>
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
<br />
  
== The Complete Config File ==
+
==== Adding a Preferences category ====
 +
We'll add a new preferences category - actually a new subcategory of the existing ''filio'' category. This enables our form to appear as a "child" of the filio section of the preferences dialog.
 +
<syntaxhighlight lang="xml">
 +
  <atom type="PreferenceCategories">
 +
    <hash type="PrefCat" key="fileio/stlio"></hash>
 +
  </atom>
 +
</syntaxhighlight>
 +
<br />
  
 +
==== Adding a Message Table entry ====
 +
We'll also need a message table entry to define the name/label for our new category.
 +
<syntaxhighlight lang="xml">
 +
  <atom type="Messages">
 +
    <hash type="Table" key="preferences.categories.en_US">
 +
      <hash type="T" key="fileio/stlio">STL I/O</hash>
 +
    </hash>
 +
  </atom>
 +
</syntaxhighlight>
 +
<br />
 +
=== Adding a CommandHelp entry ===
 +
Finally we'll add a CommandHelp entry for the stl.settings custom command, providing meaningful tooltips and description for both the command and it's arguments/attributes in addition to enabling language translation support.
 +
<syntaxhighlight lang="xml">
 +
  <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>
 +
      <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>
 +
      </hash>
 +
    </hash>
 +
  </atom>
 +
</syntaxhighlight>
 +
<br />
 +
=== The Complete Config File ===
 +
Putting it all together we now have a config file that adds the output settings for our STL saver to modo's preferences and provides some meaningful
 +
UI feedback about the custom command that implements them.
 
<syntaxhighlight lang="xml">
 
<syntaxhighlight lang="xml">
 
<?xml version="1.0" encoding="UTF-8"?>
 
<?xml version="1.0" encoding="UTF-8"?>
 
<configuration>
 
<configuration>
 +
  <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>
 +
      <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>
 +
      </list>
 +
      <atom type="Filter">prefs/fileio/stlio:filterPreset</atom>
 +
      <hash type="InCategory" key="prefs:general#head">
 +
        <atom type="Ordinal">80.8</atom>
 +
      </hash>
 +
      <atom type="Group">prefs/fileio</atom>
 +
    </hash>
 +
  </atom>
  
     <!-- Copyright 0000 Luxology, LLC. All Rights Reserved. Patents granted and pending. -->
+
  <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>
 +
    </hash>
 +
  </atom>
  
<atom type="Attributes">
+
  <atom type="PreferenceCategories">
<hash type="Sheet" key="05044994227:sheet">
+
    <hash type="PrefCat" key="fileio/stlio"></hash>
<atom type="Label">STL Object Export</atom>
+
  </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>
+
<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>
+
</list>
+
<atom type="Filter">prefs/fileio/stlio:filterPreset</atom>
+
<hash type="InCategory" key="prefs:general#head">
+
<atom type="Ordinal">80.8</atom>
+
</hash>
+
<atom type="Group">prefs/fileio</atom>
+
</hash>
+
</atom>
+
  
<atom type="Filters">
+
  <atom type="Messages">
<hash type="Preset" key="prefs/fileio/stlio:filterPreset">
+
    <hash type="Table" key="preferences.categories.en_US">
<atom type="Name">STL I/O</atom>
+
      <hash type="T" key="fileio/stlio">STL I/O</hash>
<!-- 20385740002:filterCat is the "Preferences" category -->
+
    </hash>
<atom type="Category">20385740002:filterCat</atom>
+
  </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>
+
</hash>
+
</atom>
+
 
+
<atom type="PreferenceCategories">
+
<hash type="PrefCat" key="fileio/stlio"></hash>
+
</atom>
+
  
<atom type="Messages">
 
<hash type="Table" key="preferences.categories.en_US">
 
<hash type="T" key="fileio/stlio">STL I/O</hash>
 
</hash>
 
</atom>
 
  
 
+
  <atom type="CommandHelp">
<atom type="CommandHelp">
+
    <hash type="Command" key="stl.settings@en_US">
<hash type="Command" key="stl.settings@en_US">
+
      <atom type="UserName">STL Saver settings</atom>
<atom type="UserName">STL Saver settings</atom>
+
      <atom type="ButtonName">STL Prefs</atom>
<atom type="ButtonName">STL Prefs</atom>
+
      <atom type="Desc">Command to set the output prefernces of the STL saver</atom>
<atom type="Desc">Command to set the output prefernces of the STL saver</atom>
+
      <atom type="ToolTip">Set the STL saver preferences</atom>
<atom type="ToolTip">Set the STL saver preferences</atom>
+
      <hash type="Argument" key="units">
<hash type="Argument" key="units">
+
        <atom type="UserName">Units</atom>
<atom type="UserName">Units</atom>
+
        <atom type="Desc">Selects which units the destination application will use.</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>
<atom type="ToolTip">Selects which units the destination application will use to interpret the values in the STL file.</atom>
+
      </hash>
</hash>
+
      <hash type="Argument" key="format">
<hash type="Argument" key="format">
+
        <atom type="UserName">Save ASCII</atom>
<atom type="UserName">Save ASCII</atom>
+
        <atom type="Desc">Save the file in ASCII format, default is binary.</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>
<atom type="ToolTip">Save the file in ASCII format, default is binary output.</atom>
+
      </hash>
</hash>
+
    </hash>
</hash>
+
 
   </atom>
 
   </atom>
  
 
</configuration>
 
</configuration>
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
<br /><br />
 +
 +
== See Also ==
 +
* [[Python]]
 +
* [[Saver:_Server_basics|Saver Server]]
 +
* [[LXu_TRIANGLESOUP_(index)#C16|TriangleSoup]]
 +
* [[Command System]]
 +
* [[Command Interface| Command Reference]]
 +
* [[Persistent_Data|Persistent data storage]]
 +
 +
[[Category: API Examples]]
 +
[[Category: Python API]]
 +
[[Category: UIValueHints]]
 +
[[Category: Custom Commands]]
 +
[[Category: Persistent Storage]]

Latest revision as of 17:49, 26 November 2015

Introduction

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 StringIO
from struct import Struct, pack
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
        # if we're outputting binary 's' will point to the pre-initialized 'Struct'
        # object we're using to pack the binary data - set by the saver class when
        # it creates the TriSoup object.
        self.s = 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, stype):
        # clear the verts list and return a new triangle segment.
        self.vrts = []
        return stype == 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]
            # we're using the StringIO module so we can write to the output
            # string as if it was a file
            self.output.write(self.s.pack(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 and we'll use the StringIO module instead of a raw string so
        # that we can write to it like a file (improves performance)
        if fmtASCII:
            soup.output = []
        else:
            soup.output = StringIO.StringIO()
            soup.s = Struct('12fxx')
        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()):
                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:
            try:
                fout = open(filename, 'w')
            except:
                lx.throw(lx.result.FAILED)
            else:
                fout.write('solid\n')
                fout.writelines( [x + '\n' for x in soup.output] )
                fout.write('end solid\n')
                fout.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:
            try:
                fout = open(filename, 'wb')
            except:
                lx.throw(lx.result.FAILED)
            else:
                fout.write(pack('80sl', 'Output by Luxology\'s modo', soup.polycount))
                fout.write(soup.output.getvalue())
                fout.close()
 
# 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, items):
        self._items = items
 
    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._items[0])
 
    def uiv_PopUserName(self,index):
        # returns the Username of the item at 'index'
        return self._items[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._items[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: http://modo.sdk.thefoundry.co.uk/wiki/Persistent_Data
#
# 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.saveASCII = 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.saveASCII_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 'saveASCII_val' is unset for any reason)
        try:
            return self.saveASCII_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, saveASCII):
        # appends a 'format' atom and writes the current value of the 'saveASCII_val'
        # attribute
        self.saveASCII.Append()
        self.saveASCII_val.SetInt(0, saveASCII)
 
 
 
#===============================================================================
# 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
        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.
        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_svc.AddValue(lx.symbol.sTYPE_BOOLEAN)
        persist_data.saveASCII = persist_svc.End()
        persist_data.saveASCII_val = lx.object.Attributes(persist_data.saveASCII)
 
        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:
        return
    # 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):
        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):
            persist_data.set_units(self.dyna_String(0))
        if self.dyna_IsSet(1):
            persist_data.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(persist_data.get_units())
        elif index == 1:
            va.AddInt(persist_data.get_format())
        return lx.result.OK
# bless the command to register it as a first class server (plugin)
lx.bless(CmdSTLExportSettings, "stl.settings")



Building the Configuration Entries

Adding to modo's Preferences dialog

Creating a form

We'll need a form to embed in modo's preferences dialog. It'll contain just two entries to query each of the STL saver's options defined by the stl.settings custom command. Two important items to note in the form definition are the InCategory hash which adds our form to preferences and the filter atom which allows the preferences form to dynamically place it in the correct section.

  <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>
      <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>
      </list>
      <atom type="Filter">prefs/fileio/stlio:filterPreset</atom>
      <hash type="InCategory" key="prefs:general#head">
        <atom type="Ordinal">80.8</atom>
      </hash>
      <atom type="Group">prefs/fileio</atom>
    </hash>
  </atom>


Adding a Filter Preset

Define the filter preset referenced by our form and which specifies where it should appear.

  <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>
    </hash>
  </atom>


Adding a Preferences category

We'll add a new preferences category - actually a new subcategory of the existing filio category. This enables our form to appear as a "child" of the filio section of the preferences dialog.

  <atom type="PreferenceCategories">
    <hash type="PrefCat" key="fileio/stlio"></hash>
  </atom>


Adding a Message Table entry

We'll also need a message table entry to define the name/label for our new category.

  <atom type="Messages">
    <hash type="Table" key="preferences.categories.en_US">
      <hash type="T" key="fileio/stlio">STL I/O</hash>
    </hash>
  </atom>


Adding a CommandHelp entry

Finally we'll add a CommandHelp entry for the stl.settings custom command, providing meaningful tooltips and description for both the command and it's arguments/attributes in addition to enabling language translation support.

  <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>
      <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>
      </hash>
    </hash>
  </atom>


The Complete Config File

Putting it all together we now have a config file that adds the output settings for our STL saver to modo's preferences and provides some meaningful UI feedback about the custom command that implements them.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <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>
      <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>
      </list>
      <atom type="Filter">prefs/fileio/stlio:filterPreset</atom>
      <hash type="InCategory" key="prefs:general#head">
        <atom type="Ordinal">80.8</atom>
      </hash>
      <atom type="Group">prefs/fileio</atom>
    </hash>
  </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>
    </hash>
  </atom>
 
  <atom type="PreferenceCategories">
    <hash type="PrefCat" key="fileio/stlio"></hash>
  </atom>
 
  <atom type="Messages">
    <hash type="Table" key="preferences.categories.en_US">
      <hash type="T" key="fileio/stlio">STL I/O</hash>
    </hash>
  </atom>
 
 
  <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>
      <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>
      </hash>
    </hash>
  </atom>
 
</configuration>



See Also