Difference between revisions of "Scene Saver: STL"
From The Foundry MODO SDK wiki
(→Introduction) |
(→Introduction) |
||
Line 4: | Line 4: | ||
* Using a [[LXu_TRIANGLESOUP_(index)#C16|TriangleSoup]] object 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 associated configuration 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 |
Revision as of 11:37, 18 May 2013
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 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.SetVertex(self.vdesc) 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 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 # # 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 = [] else: 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()): 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() # 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: 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 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 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.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: 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")
The Complete Config File
<?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>