Difference between revisions of "Scene Saver: STL"
From The Foundry MODO SDK wiki
(→The Complete Config File) |
(→The Complete Config File) |
||
Line 405: | Line 405: | ||
<?xml version="1.0" encoding="UTF-8"?> | <?xml version="1.0" encoding="UTF-8"?> | ||
<configuration> | <configuration> | ||
− | |||
− | |||
− | |||
<atom type="Attributes"> | <atom type="Attributes"> | ||
<hash type="Sheet" key="05044994227:sheet"> | <hash type="Sheet" key="05044994227:sheet"> |
Revision as of 22:51, 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 ....
The Full Plugin Code
# 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")
The Complete Config File
<?xml version="1.0" encoding="UTF-8"?> <?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 ""</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>