diff --git a/README.md b/README.md
index 927fac5..b4daec3 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,26 @@ Here are the steps on how to do that in Maya:

+##Job cleaning
+The job management section of the script allows you to move textures, clean out duplicates, create layers and break apart LODs. If this is all you're interested in you can just use the data folder of the scripts. Starting process by calling runJob on SimplygonJob. This will create data structures for all the textures, materials and objects that was created during job and also give you some actions you can apply to them.
+Here is a small sample script that utilizes that functionality:
+```
+import sys
+sys.path.append( 'c:/Work/GitHub/MayaPythonUI/scripts/data' )
+from simplygonjob import SimplygonJob
+from processdirectives import ProcessDirectives
+
+job = SimplygonJob()
+directives = ProcessDirectives()
+directives.settingFile = "c:/Work/GitHub/MayaPythonUI/settings/Character 3 LODs.ini"
+directives.batchMode = True
+directives.useWeights = False
+job.runJob(directives)
+#Clean out any duplicates
+job.pruneTexturesAndMaterials()
+```
+
+
##Instructions to create your own settings and XML
After you have created a number of presets (.ini files) through the Simplygon interface you need to create an XML file to wrap the setting files. An example can be viewed in the Settings folder of the repository.
diff --git a/images/overview.png b/images/overview.png
index 7b04593..3a2fec5 100644
Binary files a/images/overview.png and b/images/overview.png differ
diff --git a/scripts/SimplygonBatchProcessor.py b/scripts/SimplygonBatchProcessor.py
index 8791eb2..16c3674 100644
--- a/scripts/SimplygonBatchProcessor.py
+++ b/scripts/SimplygonBatchProcessor.py
@@ -1,15 +1,31 @@
import maya.cmds as cmds
-import maya.mel as mel
import inspect, os
-import OptimizationManagerModule
-reload(OptimizationManagerModule)
-from OptimizationManagerModule import *
+
+import model.optimizationmanager
+reload(model.optimizationmanager)
+from model.optimizationmanager import *
+import data.simplygonjob
+reload (data.simplygonjob)
+from data.simplygonjob import *
+import data.processdirectives
+reload (data.processdirectives)
+from data.processdirectives import *
+import view.optimizationpanel
+reload(view.optimizationpanel)
+from view.optimizationpanel import OptimizationPanel
+import view.browsingpanel
+reload(view.browsingpanel)
+from view.browsingpanel import BrowsingPanel
+import view.jobpanel
+reload(view.jobpanel)
+from view.jobpanel import JobPanel
+
__author__ = "Samuel Rantaeskola"
__copyright__ = "Copyright 2014, Donya Labs AB"
__credits__ = ["Samuel Rantaeskola"]
__license__ = "ALv2"
-__version__ = "0.2"
+__version__ = "0.3"
__maintainer__ = "Samuel Rantaeskola"
__email__ = "samuel@simplygon.com"
__status__ = "Prototype"
@@ -17,310 +33,132 @@
SETTINGS_FILE_SETTING = "SimplygonSettingsFileXML"
TEMP_SETTING_FILE = "__temp_processing.ini"
SIMPLYGON_LOGO = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))+"/simplygon_logo.png" #Replace this line to point out your logo
-
-"""
-Wrapper for the user weight data panel.
-"""
-class UserWeightsData:
- def __init__(self):
- self.userWeightCheckBoxCtrl = None
- self.colorSetListCtrl = None
- self.wmSliderCtrl = None
- self.wmText = None
-
- """
- Creates the panel that contains the user weights settings
- @param parentContainer: the container to add the user weight panel to
- @return: the layout that contains all the components
- """
- def createComponent(self, parentContainer):
- # Add the user weights components
- layout = cmds.frameLayout(parent= parentContainer, l="User weights", collapsable=True)
- cmds.separator(parent= layout, height=1, style="none")
- weightsLayout = cmds.rowLayout (parent= layout, numberOfColumns = 2)
- self.userWeightCheckBoxCtrl = cmds.checkBox(l="Enable", w=150, parent= weightsLayout, onc=userWeightsChanged, ofc=userWeightsChanged)
- self.colorSetListCtrl = cmds.optionMenu(parent= weightsLayout, w=350, en=False)
- cmds.separator(parent= layout, height=1, style="none")
- weightsMulLayout = cmds.rowLayout (parent= layout, numberOfColumns = 2)
- cmds.text(parent= weightsMulLayout, l="Weights multiplier", align="right", w=150)
- self.wmSliderCtrl = cmds.intSlider(min=1, max=8, value=1, step=1, parent= weightsMulLayout, w=350)
- cmds.separator(parent= layout, height=1, style="none")
- return layout
-
- """
- @return: true if the user weight checkbox is checked
- """
- def useUserWeights(self):
- return cmds.checkBox(self.userWeightCheckBoxCtrl, query = True, value=True)
-
- """
- @return: the integer value that the weight multiplier slider is set to
- """
- def getWeightMultiplier(self):
- return cmds.intSlider(self.wmSliderCtrl, query=True, value=True)
-
- """
- @return: the selected color set
- """
- def getColorSet(self):
- return cmds.optionMenu(self.colorSetListCtrl, query=True, value=True)
-
-
- """
- Should be called every time the color set selector needs to be updated. Will remove the current
- set of options and fetch the current possible sets and add them to the droplist
- """
- def updateColorSets(self):
- # Delete the current set of color sets
- try:
- menuItems = cmds.optionMenu(self.colorSetListCtrl, q=True, itemListLong=True)
- if menuItems != None and menuItems != []:
- cmds.deleteUI(menuItems)
- except:
- pass
- colorSets = cmds.polyColorSet( query=True, allColorSets=True)
- if colorSets :
- for c in colorSets:
- cmds.menuItem(parent=self.colorSetListCtrl, label=c)
- if cmds.checkBox(self.userWeightCheckBoxCtrl, query = True, value=True):
- cmds.optionMenu(self.colorSetListCtrl, edit=True, en=True)
- cmds.intSlider(self.wmSliderCtrl, edit=True, en=True)
- else:
- cmds.optionMenu(self.colorSetListCtrl, edit=True, en=False)
- cmds.intSlider(self.wmSliderCtrl, edit=True, en=False)
-
- """
- Enables/disables the user weight selection
- @param enabled: true to enable all the components.
- """
- def enable(self, enabled):
- #Only enable the color set selector if user weights are enabled
- cmds.checkBox(self.userWeightCheckBoxCtrl, edit=True, en=enabled)
- useUserWeights = cmds.checkBox(self.userWeightCheckBoxCtrl, query = True, value=True)
- cmds.optionMenu(self.colorSetListCtrl, edit=True, en=enabled and useUserWeights)
- cmds.intSlider(self.wmSliderCtrl, edit=True, en=enabled and useUserWeights)
-
"""
The main class for the Simplygon Batch processor. Handles a dock window and setting up all the components.
"""
class SimplygonBatchProcessor:
def __init__(self):
- #Start listening to selection changes to modify the color set selector
- self.WINDOW_NAME = "SimplygonBatchProcessor"
- self.DOCK_NAME =self.WINDOW_NAME+"Dock"
- self.settingsXML = ""
- self.userWeightData = UserWeightsData()
-
- #For clarity all of the controls are declared here.
- self.settingsDirCtrl = None
- self.settingsFileListCtrl = None
- self.optimizeButton = None
- self.mainLayout = None
- self.optimizationContainer = None
- self.simplygonButton = None
- self.settingsManager = None
- #END controls
-
+ self._browsingPanel = BrowsingPanel(self)
+ self._jobPanel = JobPanel(self)
+ self._optimizationPanel = OptimizationPanel(self)
+ self._settingsManager = None
+ self._settingsXML = ""
+ self._jobs = []
# Fetch the settings file folder from the environment.
if cmds.optionVar(exists= SETTINGS_FILE_SETTING):
- self.settingsXML = cmds.optionVar(q=SETTINGS_FILE_SETTING)
- self.settingsManager = OptimizationSettingsManager(self.settingsXML)
-
- """
- Opens up a browser window that allows the user to specify where you can find the XML that describes the setting files to use
- """
- def onBrowse(self, _):
- self.settingsXML = cmds.fileDialog2(fm=1, fileFilter="XML Files (*.xml)", okc="Set")[0]
- cmds.optionVar( sv=(SETTINGS_FILE_SETTING, self.settingsXML) )
- cmds.textField(self.settingsDirCtrl, edit=True, text=self.settingsXML)
- # Set up a new settings manager with the new XML.
- self.settingsManager = OptimizationSettingsManager(self.settingsXML)
- self.updateSettingFileList()
-
- """
- Refreshes the settings drop list based of the current settings in the settings manager.
- """
- def updateSettingFileList(self):
- # Delete the current set of settings
- try:
- menuItems = cmds.optionMenu(self.settingsFileListCtrl, q=True, itemListLong=True)
- if menuItems != None and menuItems != []:
- cmds.deleteUI(menuItems)
- except:
- pass
- settingNames = self.settingsManager.getSettingNames()
- for settingName in settingNames:
- cmds.menuItem(parent=self.settingsFileListCtrl, label=settingName)
- """
- Starts a Simplygon optimization in batch mode with the currently selected settings.
- """
- def onOptimize(self, _):
- self.startSimplygon(True)
-
- """
- Starts the Simplygon GUI with the currently selected settings and selected objects.
- """
- def onSimplygon(self, _):
- self.startSimplygon(False)
+ self._settingsXML = cmds.optionVar(q=SETTINGS_FILE_SETTING)
+ self._settingsManager = OptimizationSettingsManager(self._settingsXML)
+
+
+ """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+ START PROPERTIES
+ """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+ @property
+ def settingsXML(self):
+ return self._settingsXML
+
+ """
+ Sets the new settings XML file.
+ @param settingsXML: the path to the file containing the setting file information
+ """
+ def setSettingsXML(self, settingsXML):
+ self._settingsXML = settingsXML
+ cmds.optionVar( sv=(SETTINGS_FILE_SETTING, self._settingsXML) )
+ if self._settingsManager != None:
+ self._settingsManager.clear()
+ self._settingsManager = OptimizationSettingsManager(self._settingsXML)
+ self._optimizationPanel.setSettingsManager(self._settingsManager)
+
+ @property
+ def jobs(self):
+ return self._jobs
+ """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+ END PROPERTIES
+ """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
+
"""
Starts a new Simplygon process. Will generate a temporary settings file to use during this optimization.
@param batch: true if the process should be run in batch mode
"""
def startSimplygon(self, batch):
- tempPath = os.path.dirname(os.path.realpath(self.settingsXML))
+ tempPath = os.path.dirname(os.path.realpath(self._settingsXML))
tempFile = tempPath+"/"+TEMP_SETTING_FILE
tempFile = tempFile.replace("\\", "/")
# Write out a temporary settings file with the overriden settings included
with open(tempFile, 'wb') as outFile:
- self.settingsManager.writeTempConfig(outFile)
- melCmd = "Simplygon -sf \""+tempFile+"\""
- if batch:
- melCmd += " -b"
- #Check if the user weights are enabled, in that case send that along to Simplygon.
- uwEnabled = self.userWeightData.useUserWeights();
- if uwEnabled:
- colorSet = self.userWeightData.getColorSet()
- if colorSet != None:
- melCmd += " -caw \""+colorSet+"\" -wm "+ str(self.userWeightData.getWeightMultiplier())
- print melCmd
- lods = mel.eval(melCmd)
+ self._settingsManager.writeTempConfig(outFile)
+
+
+ directives = ProcessDirectives()
+ directives.settingFile = tempFile
+ directives.batchMode = batch
+ directives.useWeights = self._optimizationPanel.useUserWeights
+ directives.colorSet = self._optimizationPanel.colorSet
+ directives.weightMultiplier = self._optimizationPanel.weightMultiplier
+ job = SimplygonJob()
+ job.runJob(directives)
+ self._jobs.append(job)
#Remove the temporary processing file
os.remove(tempFile)
+ if self._jobPanel.autoClean:
+ job.pruneTexturesAndMaterials()
+ job.makeLayers()
+ job.moveTextures(self._jobPanel.textureDir)
- """
- Will update the settings components whenever the selected setting has been changed.
- """
- def settingChanged(self):
- selectedSettingName = cmds.optionMenu(self.settingsFileListCtrl, query=True, value=True)
- self.settingsManager.settingChanged(self.optimizationContainer, selectedSettingName)
- self.enable(somethingSelected())
-
- """
- Enable/disable the controls in the window
- @param enabled: true if the controls should be enabled.
- """
- def enable(self, enabled):
- cmds.button(self.optimizeButton, edit=True, en=enabled)
- cmds.button(self.simplygonButton, edit=True, en=enabled)
- if self.userWeightData != None:
- self.userWeightData.enable(enabled)
- if self.settingsManager != None:
- self.settingsManager.enable(enabled)
-
- """
- Pipes the update on to the user weight panel
- """
- def updateColorSets(self):
- self.userWeightData.updateColorSets()
-
- #START UI BUILDING
"""
- Creates the panel containing the browsing component for the setting file xml and adds it to the main layout.
- """
- def createBrowsingPanel(self):
- layout = cmds.rowLayout (parent= self.mainLayout, numberOfColumns = 2)
- self.settingsDirCtrl = cmds.textField(parent= layout, ed=False, w=400, text=self.settingsXML)
- cmds.button(parent= layout, label = "Browse", command = self.onBrowse)
-
- """
- Creates the panel containing the setting drop list and add it to the main layout.
+ Refreshes the content in all views.
+ @param enabled: true if items should be enabled.
"""
- def createSettingSelectorPanel(self):
- layout = cmds.columnLayout (parent= self.mainLayout, adjustableColumn = True)
- # Create the header
- cmds.text(parent= layout, l="Optimization settings", align="center", font="boldLabelFont")
- cmds.separator(parent= layout, height=20, style="doubleDash")
-
- #Add the settings browser component
- self.settingsFileListCtrl = cmds.optionMenu(parent= layout, cc=settingChanged)
- if len(self.settingsXML) > 0:
- self.updateSettingFileList()
- cmds.separator(parent= layout, height=20, style="none")
+ def refreshViews(self):
+ self._optimizationPanel.updateColorSets()
+ enable = True
+ selection = cmds.ls(sl=1)
+ if selection == None or selection==[]:
+ enable = False
+ self._optimizationPanel.enable(enable)
"""
Creates the main window
+ @param parentContainer: the container to add the panel to
"""
- def setupWindow(self):
- #Delete the window if there already is an open instance
- if cmds.window(self.WINDOW_NAME, exists=True):
- cmds.deleteUI(self.WINDOW_NAME)
- if cmds.dockControl(self.DOCK_NAME, exists=True):
- cmds.deleteUI(self.DOCK_NAME)
-
- window = cmds.window(self.WINDOW_NAME, title="Batch Processor", iconName="DL" )
- self.mainLayout = cmds.columnLayout (adjustableColumn = True)
-
- # Add the simplygon logo for graphical splendor.
- cmds.image(parent = self.mainLayout, image=SIMPLYGON_LOGO, w=300)
-
- # Add the components that shows allows for browsing to the setting file directory
- self.createBrowsingPanel()
-
- # Add the setting selector panel
- self.createSettingSelectorPanel()
-
- self.optimizationContainer = cmds.columnLayout (parent= self.mainLayout, adjustableColumn = True)
- self.userWeightData.createComponent(self.optimizationContainer)
-
- #Add the optimization button
- endLayout = cmds.columnLayout (parent = self.mainLayout, adjustableColumn = True)
- cmds.separator(parent= endLayout, height=20, style="none")
- self.optimizeButton = cmds.button(parent= endLayout, label="Optimize", c=self.onOptimize)
-
- cmds.separator(parent= endLayout, height=20, style="none")
- self.simplygonButton = cmds.button(parent= endLayout, label="Send to Simplygon", c=self.onSimplygon)
-
- #Force an update of the color set selector
- self.updateColorSets()
- # Force an update of the setting selector
- if self.settingsManager != None:
- self.settingChanged()
- else:
- self.enable(False)
-
- cmds.dockControl(self.DOCK_NAME, area="right", content=window, l="Batch Processor", width=500)
-
-# This is ugly as hell, but since Maya seems to randomly crash when events are triggered on member functions we pipe them outside.
-batchProcessor = ""
+ def createContent(self, parentContainer):
+ self._browsingPanel.createPanel(parentContainer)
+ self._optimizationPanel.createPanel(parentContainer)
+ self._jobPanel.createPanel(parentContainer)
+ self._optimizationPanel.setSettingsManager(self._settingsManager)
+ if self._settingsManager == None:
+ self.enable(False)
+
+WINDOW_NAME = "SimplygonBatchProcessor"
+DOCK_NAME = WINDOW_NAME+"Dock"
+
"""
-Called when there is a change in selection in the settings drop list
-"""
-def settingChanged(_):
- batchProcessor.settingChanged()
-
-"""
-Called when the user enables/disables the user weight check box.
-"""
-def userWeightsChanged(_):
- batchProcessor.updateColorSets()
-
-"""
-@return: true if something is currently selected.
-"""
-def somethingSelected():
- selection = cmds.ls(sl=1)
- if selection == None or selection==[]:
- return False
- else:
- return True
-
-"""
-Called when something is selected in the main viewport. Forces an update of the color selector and enabling/disabling controls
+Function to expose functionality outside of this module
"""
-def selectionChanged():
- batchProcessor.updateColorSets()
- batchProcessor.enable(somethingSelected())
-
+def createContent(parentContainer):
+ batchProcessor = SimplygonBatchProcessor()
+ batchProcessor.createContent(parentContainer)
+ return batchProcessor
+
"""
Main function to start the plugin.
"""
def openSimplygonBatchProcessor():
- global batchProcessor
- batchProcessor = SimplygonBatchProcessor()
- batchProcessor.setupWindow()
+ #Delete the window if there already is an open instance
+ if cmds.window(WINDOW_NAME, exists=True):
+ cmds.deleteUI(WINDOW_NAME)
+ if cmds.dockControl(DOCK_NAME, exists=True):
+ cmds.deleteUI(DOCK_NAME)
+
+ window = cmds.window(WINDOW_NAME, title="Batch Processor", iconName="DL" )
+ layout = cmds.columnLayout (adjustableColumn = True)
+ dock = cmds.dockControl(DOCK_NAME, area="right", content=window, l="Batch Processor", width=500)
+ # Add the simplygon logo for graphical splendor.
+ cmds.image(parent = layout, image=SIMPLYGON_LOGO, w=300)
+ batchProcessor = createContent(layout)
# Start a script job that listens to selection changed events.
- cmds.scriptJob( event= ["SelectionChanged",selectionChanged], protected=True, parent = batchProcessor.DOCK_NAME)
+ cmds.scriptJob( event= ["SelectionChanged",batchProcessor.refreshViews], protected=True, parent = dock)
diff --git a/scripts/data/__init__.py b/scripts/data/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scripts/data/materialcollection.py b/scripts/data/materialcollection.py
new file mode 100644
index 0000000..5a71e52
--- /dev/null
+++ b/scripts/data/materialcollection.py
@@ -0,0 +1,273 @@
+import maya.cmds as cmds
+import utils.mutils
+reload (utils.mutils)
+import utils.mutils as mutils
+from texturecollection import *
+
+
+targetsToIgnore = ['defaultTextureList', 'place2dTexture']
+"""
+Class that stores the information about a connection between a material and a texture.
+"""
+class MaterialTextureConnection:
+ """
+ Constructor.
+ @param materialAttribute: the attribute on the material involved in the connection (whole string, object and attribute)
+ @param textureData: the data object for the connected texture
+ @param textureAttribute: the attribute of the connected texture (whole string, object and attribute)
+ """
+ def __init__(self, materialAttribute, textureData, textureAttribute):
+ self._materialAttribute = materialAttribute
+ self._textureData = textureData
+ self._textureAttribute = mutils.getAttribute(textureAttribute)
+
+ """
+ Returns the texture data of the connected texture
+ """
+ @property
+ def texture(self):
+ return self._textureData
+
+ """
+ Copies the connections from the current texture to the new tetxture.
+ @param newTexture: the texture to copy all connnections to.
+ """
+ def copyConnections(self, newTexture):
+ # List all connections from the current texture, with the target of the connection as well.
+ cons = cmds.listConnections(self._textureData.name, c=True)
+ for sourceAttr, target in mutils.pairwise(cons):
+ # Get the name of the attribute
+ attribute = mutils.getAttribute(sourceAttr)
+ # Get the object type of the connected object
+ targetType = cmds.objectType(target)
+ # Do not copy connection if the target is to be ignored
+ if targetType not in targetsToIgnore:
+ #Get the destination attributes on the target object.
+ destinationAttributes = cmds.connectionInfo(sourceAttr,dfs=True)
+ for da in destinationAttributes:
+ #Disconnect the current texture and connect it to the new texture
+ mutils.moveConnection(sourceAttr, newTexture.name+"."+attribute, da)
+
+ """
+ Will re-route this materials connection to a new texture if needed.
+ @param textureCollection: the collection of textures.
+ """
+ def rerouteConnection(self, textureCollection):
+ #Check if the current texture should be redirected
+ newTexture = textureCollection.redirectTo(self._textureData)
+ if newTexture != None:
+ # Disconnect the currently used texture and connect the new texture to this material
+ mutils.moveConnection(self._textureData.name+"."+self._textureAttribute, newTexture.name+"."+self._textureAttribute, self._materialAttribute)
+ # Copy any other connections that might exit on this texture
+ self.copyConnections(newTexture)
+ # Set the new texture on this connection
+ self._textureData = newTexture
+
+"""
+Class that contains all information about a material
+"""
+class MaterialData:
+ """
+ Constructor
+ @param materialName: the name of this material
+ @param textureCollection: the collection of all textures
+ """
+ def __init__(self, materialName, textureCollection):
+ self._name = materialName
+ self._textureConnections = []
+ # List all directly connected textures
+ matType = cmds.objectType(materialName)
+ cons = cmds.listConnections(materialName, type="file", c=True)
+ if cons != None:
+ for attribute, texture in mutils.pairwise(cons):
+ # Find the source attribute on the texture in the connection
+ textureSourceAttribute = mutils.getConnectedAttribute(attribute,False)
+ textureData = textureCollection.getTextureData(texture)
+ if textureData == None:
+ raise ValueError("Something has gone wrong as we couldn't find the texture data: "+texture+" for: "+materialName)
+ else:
+ # Tuple of attributes in the texture that is connected to the material data
+ self._textureConnections.append(MaterialTextureConnection(attribute, textureData, textureSourceAttribute))
+ # Check if there is a normal map
+ bumps = cmds.listConnections(materialName, type="bump2d")
+ if bumps != None:
+ for b in bumps:
+ tCons = cmds.listConnections(b, type="file", c=True)
+ if tCons != None:
+ bumpAttribute = tCons[0]
+ texture = tCons[1]
+ textureData = textureCollection.getTextureData(texture)
+ if textureData == None:
+ raise ValueError("Something has gone wrong as we couldn't find the bump texture data: "+texture+" for: "+materialName)
+ textureSourceAttribute = mutils.getConnectedAttribute(bumpAttribute,False)
+ self._textureConnections.append(MaterialTextureConnection(tCons[0], textureData, textureSourceAttribute))
+
+ """
+ Returns the name of this material
+ """
+ @property
+ def name(self):
+ return self._name
+
+ """
+ Returns the list of texture connections on this material
+ """
+ @property
+ def texturesConnections(self):
+ return self._textureConnections
+
+ """
+ Returns a list of names to the textures connected to this material
+ """
+ @property
+ def textureNames(self):
+ textureNames = []
+ for tc in self._textureConnections:
+ textureNames.append(tc.texture.name)
+ return textureNames
+
+ """
+ Returns true if this material is a duplicate of the incoming material. This is whenever the two materials are using the same
+ textures, or textures that have the same hashvalue.
+ @param material2: the material to compare this material to
+ """
+ def isEqual(self, material2):
+ for t1 in self.texturesConnections:
+ exists = False
+ for t2 in material2.texturesConnections:
+ if t1.texture.isEqual(t2.texture):
+ exists = True
+ if not exists:
+ return False
+ for t1 in material2.texturesConnections:
+ exists = False
+ for t2 in self.texturesConnections:
+ if t1.texture.isEqual(t2.texture):
+ exists = True
+ if not exists:
+ return False
+ return True
+
+ """
+ Will loop through all connections on this material and re-route all textures that are duplicates.
+ @param textureCollection: the collection of textures
+ """
+ def rerouteConnections(self, textureCollection):
+ for tc in self._textureConnections:
+ tc.rerouteConnection(textureCollection)
+
+ """
+ Will remove any connections that are not valid anymore, i.e. pointing to objects that doesn't exist.
+ """
+ def removeInvalidConnections(self):
+ toRemove = []
+ for tc in self._textureConnections:
+ if not cmds.objExists(tc.texture.name):
+ toRemove.append(tc)
+ for tc in toRemove:
+ self._textureConnections.remove(tc)
+
+"""
+A material collection contains all the material data that is created when a simplygon job is run.
+Before starting a job you need to run takeSnapShot so that it can store all materials that exists before.
+After a job is done you need to run calculateDiff so that the material collection is pruned to only the materials
+that was created during the job.
+"""
+class MaterialCollection:
+ def __init__(self):
+ self._materialData = []
+ self._duplicates = []
+ self._textureCollection = None
+ """
+ Takes a snapshot of the materials in the scene as it is right now.
+ """
+ def takeSnapShot(self):
+ self.snapShot = cmds.ls( materials=True)
+
+ """
+ Compares the materials in the current scene with the snapshot and stores a material data for all
+ materials that has been added to the scene.
+ It will also calculate duplicates for easy removal.
+ @param textureCollection: the collection of all textures. Used to link materials to the correct texture datas.
+ """
+ def calculateDiff(self,textureCollection):
+ # Time to find out which materials were added
+ self._textureCollection = textureCollection
+ materialsNow = cmds.ls( materials=True)
+ for m in materialsNow:
+ if m not in self.snapShot:
+ self._materialData.append(MaterialData(m, self._textureCollection))
+ self.calculateDuplicates()
+
+ """
+ Groups the newly created materials by duplicates. Can only been called after a snapshot and a diff
+ has been made.
+ """
+ def calculateDuplicates(self):
+ self._duplicates = []
+ for md in self._materialData:
+ added = False
+ for l in self._duplicates:
+ #The first material in the list is considered the base
+ if l[0].isEqual(md):
+ l.append(md)
+ added = True
+ if not added:
+ self._duplicates.append([md])
+
+ """
+ Reconnects all materials pointing to duplicate textures to new textures.
+ """
+ def rerouteMaterials(self):
+ for m in self._materialData:
+ m.rerouteConnections(self._textureCollection)
+
+ """
+ @param name: name of the material to get the data for
+ """
+ def getMaterialData(self, name):
+ for md in self._materialData:
+ if md.name == name:
+ return md
+ return None
+
+ """
+ Returns the material data to relink the incoming data to if it's a duplicate and not the base material.
+ If the incoming material is a base or not a duplicate, None will be returned.
+ @param materialData: The material to check if it should be replaced
+ """
+ def redirectTo(self, materialData):
+ for l in self._duplicates:
+ for i in range(1, len(l)):
+ if l[i] == materialData:
+ return l[0]
+ #No need to replace the material
+ return None
+
+ """
+ Deletes all materials that are duplicates for the scene. Should only be called after the objects have been
+ reconnected to new materials.
+ """
+ def deleteDuplicates(self):
+ #Loop over all the duplicates and delete all but one material in each bucket
+ for l in self._duplicates:
+ for i in range(1, len(l)):
+ cmds.delete(l[i].name)
+ self._materialData.remove(l[i])
+ self._duplicates = []
+
+ """
+ Removes any materials that doesn't exist in the scene anymore.
+ """
+ def removeInvalidAssets(self):
+ toRemove = []
+ for md in self._materialData:
+ if not cmds.objExists(md.name):
+ toRemove.append(md)
+ else:
+ md.removeInvalidConnections()
+ for md in toRemove:
+ self._materialData.remove(md)
+
+ # We need to update the list of duplicates
+ self.calculateDuplicates()
diff --git a/scripts/data/objectcollection.py b/scripts/data/objectcollection.py
new file mode 100644
index 0000000..ce14e82
--- /dev/null
+++ b/scripts/data/objectcollection.py
@@ -0,0 +1,340 @@
+import maya.cmds as cmds
+import sys
+from materialcollection import *
+import utils.mutils as mutils
+
+
+"""
+Stores the information about a connection between a shading engine and a material
+"""
+class ShadingEngineMaterialConnection:
+ """
+ Constructor
+ @param materialAttribute: the attribute on the material connected to the shader
+ @param materialData: the data describing the connected material
+ @param shadingEngineAttribute: the attribute on the shader connected to the material
+ @param shadingEngine: the shader
+ """
+ def __init__(self, materialAttribute, materialData, shadingEngineAttribute, shadingEngine):
+ self._materialAttribute = materialAttribute
+ self._materialData = materialData
+ self._shadingEngineAttribute = shadingEngineAttribute
+ self._shadingEngine = shadingEngine
+
+ """
+ Returns the attribute on the material
+ """
+ @property
+ def materialAttribute(self):
+ return self._materialAttribute
+
+ """
+ Returns the data describing the connected material
+ """
+ @property
+ def materialData(self):
+ return self._materialData
+
+ """
+ Returns the attribute on the shader that is connected to the material
+ """
+ @property
+ def shadingEngineAttribute(self):
+ return self._shadingEngineAttribute
+
+ """
+ Returns the shader name
+ """
+ @property
+ def shadingEngine(self):
+ return self._shadingEngine
+
+ """
+ Reroutes the shapes material connection to a new material if needed.
+ @param materialCollection: the collection of all materials
+ """
+ def rerouteShape(self, materialCollection):
+ newMaterial = materialCollection.redirectTo(self.materialData)
+ if newMaterial:
+ # The current material needs to be replaced. Disconnect that one and connect the shader to the new material.
+ mutils.moveConnection(self.materialData.name+"."+self.materialAttribute, newMaterial.name+"."+self.materialAttribute, self.shadingEngine+"."+self.shadingEngineAttribute)
+ self._materialData = newMaterial
+
+SURFACESHADERATTR = 'surfaceShader'
+"""
+The data about a shape in an object
+"""
+class ShapeData:
+ """
+ Constructor
+ @param shapeName: the name of this shape
+ @param materialCollection: the collection of all materials
+ """
+ def __init__(self, shapeName, materialCollection):
+ shadingEngines = cmds.listConnections(shapeName, type='shadingEngine')
+ self._shapeName = shapeName
+ self._materialConnections = []
+ if shadingEngines != None:
+ for se in shadingEngines:
+ materialAttribute = cmds.connectionInfo(se+'.'+SURFACESHADERATTR, sfd=True)
+ matAndAttr = mutils.getObjectAndAttribute(materialAttribute)
+ materialData = materialCollection.getMaterialData(matAndAttr[0])
+ if materialData != None:
+ self._materialConnections.append(ShadingEngineMaterialConnection(matAndAttr[1], materialData, SURFACESHADERATTR, se))
+
+ """
+ Will reroute all shaders in this shape to new materials if needed.
+ @param materialCollection: the collection of all materials
+ """
+ def rerouteShape(self, materialCollection):
+ for mc in self._materialConnections:
+ mc.rerouteShape(materialCollection)
+
+ """
+ Returns the names of all materials connected to this shape
+ """
+ @property
+ def materialNames(self):
+ materialNames = []
+ for mc in self._materialConnections:
+ materialNames.append(mc.materialData.name)
+ return materialNames
+
+ """
+ Returns the names of all textures connected to this shape
+ """
+ @property
+ def textureNames(self):
+ textureNames = []
+ for mc in self._materialConnections:
+ textureNames.extend(mc.materialData.textureNames)
+ return textureNames
+
+ """
+ Cleans out any connections to materials that does not exist any more.
+ """
+ def removeInvalidConnections(self):
+ toRemove = []
+ for mc in self._materialConnections:
+ if not cmds.objExists(mc.materialData.name):
+ toRemove.append(mc)
+ for mc in toRemove:
+ self._materialConnections.remove(mc)
+
+LOD_KEYWORD = '_LOD'
+"""
+Contains data about an object
+"""
+class ObjectData:
+ """
+ Constructor.
+ @param objectName: name of this object
+ @param materialCollection: the collection of all materials
+ """
+ def __init__(self, objectName, materialCollection):
+ #Find all connected shapes
+ shapeNames = cmds.listRelatives(objectName, path=True)
+ self._shapes = []
+ self._objectName = objectName
+ #Which lod is this part of?
+ lodStrIndex = objectName.rfind(LOD_KEYWORD)
+ if lodStrIndex != -1:
+ self._LODNum = int(objectName[lodStrIndex+len(LOD_KEYWORD):])
+ else:
+ self._LODNum = -1
+ #Create shapes data for all connected shapes
+ if shapeNames != None:
+ for s in shapeNames:
+ self._shapes.append(ShapeData(s, materialCollection))
+
+ """
+ Reroutes all the connections to materials in subshapes
+ @param materialCollection: the collection of all materials
+ """
+ def rerouteObject(self, materialCollection):
+ for s in self._shapes:
+ s.rerouteShape(materialCollection)
+
+ """
+ Returns the LOD index of this object
+ """
+ @property
+ def LODNum(self):
+ return self._LODNum
+
+ """
+ Returns the name of this object
+ """
+ @property
+ def name(self):
+ return self._objectName
+
+ """
+ Returns the names of all materials connected to this object
+ """
+ @property
+ def materialNames(self):
+ materialNames = []
+ for s in self._shapes:
+ materialNames.extend(s.materialNames)
+ return materialNames
+
+ """
+ Returns the names of all textures connected to this object
+ """
+ @property
+ def textureNames(self):
+ textureNames = []
+ for s in self._shapes:
+ textureNames.extend(s.textureNames)
+ return textureNames
+
+ """
+ Removes any invalid connections to materials in this object
+ """
+ def removeInvalidConnections(self):
+ for s in self._shapes:
+ s.removeInvalidConnections()
+
+"""
+Stores information about all the objects within a lod index
+"""
+class LODData:
+ """
+ Constructor
+ @param index: the index of this LOD data
+ """
+ def __init__(self,index):
+ self._objectData = []
+ self._index = index
+
+ """
+ Adds an object to this LOD.
+ @param obj: the object to add to this LOD
+ """
+ def addObject(self,obj):
+ self._objectData.append(obj)
+
+ """
+ Returns the names of all the objects in this LOD
+ """
+ @property
+ def objectNames(self):
+ objectNames = []
+ for o in self._objectData:
+ objectNames.append(o.name)
+ return objectNames
+
+ """
+ Returns the names of all the materials in this LOD
+ """
+ @property
+ def materialNames(self):
+ materialNames = []
+ for o in self._objectData:
+ materialNames.extend(o.materialNames)
+ return materialNames
+
+ """
+ Returns the names of all the textures in this LOD
+ """
+ @property
+ def textureNames(self):
+ textureNames = []
+ for o in self._objectData:
+ textureNames.extend(o.textureNames)
+ return textureNames
+
+ """
+ Translates this LOD out in x direction to break LODs apart.
+ """
+ def splitOut(self):
+ objs = self.objectNames
+ bbox = cmds.exactWorldBoundingBox(objs)
+ for n in objs:
+ objAttr = n+".translateX"
+ translate = (self._index+1)*((bbox[3]-bbox[0])*1.1)
+ cmds.setAttr(objAttr, lock=False)
+ cmds.setAttr(objAttr, translate)
+
+"""
+Contains all the objects that was created during a SImplygon job.
+"""
+class ObjectCollection:
+ def __init__(self):
+ self._objectData = []
+ self._materialCollection = None
+
+ """
+ Takes a snapshot of the objects in the scene as it is right now.
+ """
+ def takeSnapShot(self):
+ self.objects = cmds.ls( type='transform', l=True)
+
+ """
+ Compares the objects in the current scene with the snapshot and stores a objects data for all
+ objects that has been added to the scene.
+ @param materialCollection: the collection of all materials. Used to link objects to the correct material datas.
+ """
+ def calculateDiff(self, materialCollection):
+ self._materialCollection = materialCollection
+ # Time to find out which objects were added
+ objectsNow = cmds.ls( type='transform', l=True)
+ for o in objectsNow:
+ if o not in self.objects:
+ self._objectData.append(ObjectData(o, self._materialCollection))
+ self.buildLods()
+
+ """
+ Rerouts all connections in objects to new materials if needed.
+ """
+ def rerouteObjects(self):
+ for o in self._objectData:
+ o.rerouteObject(self._materialCollection)
+
+ """
+ Breaks the objects into LODs according to their LOD number
+ """
+ def buildLods(self):
+ # Since there can be several jobs in one scene, LOD numbers can grow. We need to find the lowest num in this scene.
+ lowestLODNum = sys.maxint
+ for o in self._objectData:
+ if o.LODNum != -1 and o.LODNum < lowestLODNum:
+ lowestLODNum = o.LODNum
+ self._LODs = {}
+ for o in self._objectData:
+ if o.LODNum != -1:
+ lodIndex = o.LODNum-lowestLODNum
+ if lodIndex not in self._LODs:
+ self._LODs[lodIndex] = LODData(lodIndex)
+ lodData = self._LODs[lodIndex]
+ lodData.addObject(o)
+
+ """
+ Translates the LODs out to break them apart.
+ """
+ def splitLODs(self):
+ for l in self._LODs:
+ self._LODs[l].splitOut()
+
+ """
+ Returns a list of all the lods
+ """
+ def getLODs(self):
+ return self._LODs
+
+ """
+ Removes any invalid object or connections in the object collection.
+ """
+ def removeInvalidAssets(self):
+ toRemove = []
+ for od in self._objectData:
+ if not cmds.objExists(od.name):
+ toRemove.append(od)
+ else:
+ od.removeInvalidConnections()
+ for od in toRemove:
+ self._objectData.remove(od)
+ # We need to rebuild the LOD data
+ self.buildLods()
+
\ No newline at end of file
diff --git a/scripts/data/processdirectives.py b/scripts/data/processdirectives.py
new file mode 100644
index 0000000..d5f964d
--- /dev/null
+++ b/scripts/data/processdirectives.py
@@ -0,0 +1,60 @@
+"""
+Class that contains the directives that are used to initiate a Simplygon job.
+"""
+class ProcessDirectives:
+ def __init__(self):
+ self._batchMode = False
+ self._settingFile = ""
+ self._useWeights = False
+ self._colorSet = ""
+ self._weightMultiplier = 1
+
+ """
+ Property that specifies whether to use custom color sets from Maya when starting the Simplygon process. Should always be
+ set in combination with colorSet.
+ """
+ @property
+ def useWeights(self):
+ return self._useWeights
+ @useWeights.setter
+ def useWeights(self, enabled):
+ self._useWeights = enabled
+
+ """
+ The color set that should be used as custom weights
+ """
+ @property
+ def colorSet(self):
+ return self._colorSet
+ @colorSet.setter
+ def colorSet(self, setName):
+ self._colorSet = setName
+
+ """
+ The multiplier to increase the power of the user weights by.
+ """
+ @property
+ def weightMultiplier(self):
+ return self._weightMultiplier
+ @weightMultiplier.setter
+ def weightMultiplier(self, multiplier):
+ self._weightMultiplier = multiplier
+ """
+ The path to the settingfile to use during the job
+ """
+ @property
+ def settingFile(self):
+ return self._settingFile
+ @settingFile.setter
+ def settingFile(self, filePath):
+ self._settingFile = filePath
+
+ """
+ True if the process should be run without going through the SimplygonGUI.
+ """
+ @property
+ def batchMode(self):
+ return self._batchMode
+ @batchMode.setter
+ def batchMode(self, enabled):
+ self._batchMode = enabled
diff --git a/scripts/data/simplygonjob.py b/scripts/data/simplygonjob.py
new file mode 100644
index 0000000..f8ad0bb
--- /dev/null
+++ b/scripts/data/simplygonjob.py
@@ -0,0 +1,137 @@
+from time import gmtime, strftime
+import maya.mel as mel
+import texturecollection
+import materialcollection
+import objectcollection
+reload (texturecollection)
+reload (materialcollection)
+reload (objectcollection)
+
+from texturecollection import *
+from materialcollection import *
+from objectcollection import *
+
+"""
+Stores all the data that is generated during a Simplygon job.
+"""
+class SimplygonJob:
+ def __init__(self):
+ self._directives = None
+ self._time = strftime("%H:%M:%S", gmtime())
+ self._textureCollection = None
+ self._materialCollection = None
+ self._objectCollection = None
+
+ """
+ Returns the time this job was started as a string
+ """
+ @property
+ def time(self):
+ return self._time
+
+ """
+ Returns the name of this job
+ """
+ @property
+ def name(self):
+ # Time will suffice as description for now
+ return self.time
+
+ """
+ Returns the directives that was used to run this job
+ """
+ @property
+ def directives(self):
+ return self._directives
+
+ """
+ Will start a Simplygon process with the specified directives
+ @param directives: the settings to use during the process
+ """
+ def runJob(self, directives):
+ melCmd = "Simplygon -sf \""+directives.settingFile+"\" "
+ if directives.batchMode:
+ melCmd += "-b "
+ #Check if the user weights are enabled, in that case send that along to Simplygon.
+ if directives.useWeights:
+ if directives.colorSet != None:
+ melCmd += "-caw \""+directives.colorSet+"\" -wm "+ str(directives.weightMultiplier)
+ self._directives = directives
+
+ #Take a snapshot of the scene before the process starts
+ self._textureCollection = TextureCollection()
+ self._materialCollection = MaterialCollection()
+ self._objectCollection = ObjectCollection()
+ self._textureCollection.takeSnapShot()
+ self._materialCollection.takeSnapShot()
+ self._objectCollection.takeSnapShot()
+
+ #Run the simplygon job
+ result = mel.eval(melCmd)
+
+ #Find out what Simplygon created
+ self._textureCollection.calculateDiff()
+ self._materialCollection.calculateDiff(self._textureCollection)
+ self._objectCollection.calculateDiff(self._materialCollection)
+
+
+ """
+ Returns the LOD's generated by this process. As a dictionary with lod index as key of object data lists.
+ """
+ def getLODs(self):
+ return self._objectCollection.getLODs()
+
+ """
+ Generates a layer per LOD.
+ """
+ def makeLayers(self):
+ lods = self._objectCollection.getLODs()
+ for lod in lods:
+ layer = cmds.createDisplayLayer(empty=True, n="LOD"+str(lod))
+ cmds.editDisplayLayerMembers(layer, lods[lod].objectNames)
+
+ """
+ Cleans out duplicate materials and textures and reroutes objects and materials.
+ """
+ def pruneTexturesAndMaterials(self):
+ self._materialCollection.rerouteMaterials()
+ self._objectCollection.rerouteObjects()
+ self._textureCollection.deleteDuplicates()
+ self._materialCollection.deleteDuplicates()
+
+ """
+ Breaks up the LODs in the X axis.
+ """
+ def splitLODs(self):
+ self._objectCollection.splitLODs()
+
+ """
+ Moves all textures generated by this job to the specified location.
+ @param directory: the directory to move all textures to
+ """
+ def moveTextures(self, directory):
+ if directory == None or directory == "":
+ raise RuntimeError("No directory specified to move textures to")
+
+ self._textureCollection.moveTextures(directory)
+
+ """
+ Not yet implemented. Should allow to rename objects, materials, textures by replacing parts of their names.
+ """
+ def rename(self, replaceString, newString):
+ pass
+
+ """
+ Cleans the data sets of any objects, materials or textures that have been removed from the scene.
+ """
+ def removeInvalidAssets(self):
+ self._objectCollection.removeInvalidAssets()
+ self._textureCollection.removeInvalidAssets()
+ self._materialCollection.removeInvalidAssets()
+
+ """
+ Not yet implemented. Should return true if there is no assets left in this job.
+ """
+ def isEmpty(self):
+ return False
+
\ No newline at end of file
diff --git a/scripts/data/texturecollection.py b/scripts/data/texturecollection.py
new file mode 100644
index 0000000..ee5c0aa
--- /dev/null
+++ b/scripts/data/texturecollection.py
@@ -0,0 +1,174 @@
+import maya.cmds as cmds
+import os
+import shutil
+import hashlib
+
+"""
+Will calculate the hash value for the incoming file using the defined hasher.
+@param aFile: the file to calculate hashvalue for
+@param hasher: the hasher to use
+@return: the hash value for the file
+"""
+def hashfile(afile, hasher, blocksize=65536):
+ buf = afile.read(blocksize)
+ while len(buf) > 0:
+ hasher.update(buf)
+ buf = afile.read(blocksize)
+ return hasher.digest()
+
+"""
+Class that contains the information about a texture.
+"""
+class TextureData:
+ """
+ Construtor.
+ @param textureName: the name for this texture
+ """
+ def __init__(self, textureName):
+ self._name = textureName
+ self._materialConnection = None
+ self._filePath = cmds.getAttr(textureName+'.fileTextureName')
+ self._hash = hashfile(open(self._filePath , 'rb'), hashlib.sha256())
+
+ """
+ Returns if this texture is equal or as the incoming texture. Either they're pointing the same file or
+ the files have the same hash value.
+ @param textureData: the texture to compare with
+ @return: true if the textures are equal
+ """
+ def isEqual(self, textureData):
+ return self.name == textureData.name or self.filePath == textureData.filePath or self.hash == textureData.hash
+
+ """
+ Returns the name of this texture
+ """
+ @property
+ def name(self):
+ return self._name
+
+ """
+ Returns the hash value of the texture file
+ """
+ @property
+ def hash(self):
+ return self._hash
+
+ """
+ Returns the path to the texture file
+ """
+ @property
+ def filePath(self):
+ return self._filePath
+
+ """
+ Moves the texture file to directory and relinks the texture object
+ @directory: the folder to move the texture file to
+ """
+ def moveTo(self, directory):
+ fileName = os.path.basename(os.path.realpath(self.filePath))
+ destination = directory+"/"+fileName
+ shutil.move(self.filePath, destination)
+ self._filePath = destination
+ cmds.setAttr(self.name+'.fileTextureName', self._filePath, type="string")
+
+"""
+Class that stores all textures generated in a simplygon job.
+"""
+class TextureCollection:
+ def __init__(self):
+ self._textureData = []
+ self._duplicates = {}
+
+ """
+ Takes a snapshot of the textures in the scene as it is right now.
+ """
+ def takeSnapShot(self):
+ self._textures = cmds.ls( textures=True)
+
+
+ """
+ Compares the textures in the current scene with the snapshot and stores a texture data for all
+ textures that has been added to the scene.
+ It will also calculate duplicates for easy removal.
+ """
+ def calculateDiff(self):
+ # Time to find out which textures were added
+ texturesNow = cmds.ls( textures=True)
+ for t in texturesNow:
+ if t not in self._textures:
+ self._textureData.append(TextureData(t))
+ self.calculateDuplicates()
+
+ """
+ Organizes the textures by hash value so that textures can be easily replaced.
+ """
+ def calculateDuplicates(self):
+ self._duplicates = {}
+ #Group all textures by their hash value to sort out duplications
+ for td in self._textureData:
+ if(td.hash not in self._duplicates):
+ self._duplicates[td.hash] = []
+ self._duplicates[td.hash].append(td)
+
+ """
+ Returns the texture with matching the incoming name.
+ @param name: name of the texture to get
+ """
+ def getTextureData(self, name):
+ for td in self._textureData:
+ if td.name == name:
+ return td
+ return None
+
+ """
+ Returns the texture data to relink the incoming data to if it's a duplicate and not the base texture.
+ If the incoming texture is a base or not a duplicate, None will be returned.
+ @param textureData: The texture to check if it should be replaced
+ """
+ def redirectTo(self, textureData):
+ if textureData.hash in self._duplicates and len(self._duplicates[textureData.hash]) >1:
+ replacement = self._duplicates[textureData.hash][0]
+ if textureData.name != replacement:
+ return replacement
+ #No need to replace the texture
+ return None
+
+ """
+ Will clean out all the duplicate textures also deleting the files
+ """
+ def deleteDuplicates(self):
+ #Loop over all the duplicates and delete all but one texture in each bucket
+ for hash in self._duplicates:
+ tdList = self._duplicates[hash]
+ for i in range(1, len(tdList)):
+ td = tdList[i]
+ cmds.delete(td.name)
+ try:
+ os.remove(td.filePath)
+ except OSError:
+ pass
+ self._textureData.remove(td)
+ self._duplicates = {}
+
+ """
+ Will clear this collection of any textures that doesn't exist in the scene anymore
+ """
+ def removeInvalidAssets(self):
+ toRemove = []
+ for td in self._textureData:
+ if not cmds.objExists(td.name):
+ toRemove.append(td)
+ for td in toRemove:
+ self._textureData.remove(td)
+
+ # We need to update the list of duplicates
+ self.calculateDuplicates()
+
+ """
+ Will move all textures in this collection to the specified directory
+ @param directory: the directory to move all textures to.
+ """
+ def moveTextures(self, directory):
+ for td in self._textureData:
+ td.moveTo(directory)
+
diff --git a/scripts/model/__init__.py b/scripts/model/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scripts/OptimizationManagerModule.py b/scripts/model/optimizationmanager.py
similarity index 69%
rename from scripts/OptimizationManagerModule.py
rename to scripts/model/optimizationmanager.py
index 3e4c11f..912a636 100644
--- a/scripts/OptimizationManagerModule.py
+++ b/scripts/model/optimizationmanager.py
@@ -2,9 +2,9 @@
import maya.cmds as cmds
import inspect, os
import xml.etree.ElementTree as etree
-import KeyModifierModule
-reload(KeyModifierModule)
-from KeyModifierModule import *
+import view.keymodifier
+reload(view.keymodifier)
+from view.keymodifier import *
"""
Class that contains data about a section in the settings XML. It can also generate for the interface.
@@ -38,7 +38,7 @@ def createComponent(self, parentContainer):
font = "boldLabelFont"
if self.indentLevel >= 1:
font = "tinyBoldLabelFont"
- layout = cmds.frameLayout(parent= parentContainer, l=self.description, collapsable=True, li=self.indentLevel*10, font=font)
+ layout = cmds.frameLayout(parent= parentContainer, l=self.description, collapsable=True, collapse=True, li=self.indentLevel*10, font=font)
for child in self.children:
#Add some air between the keys
if not isinstance(child, SectionData):
@@ -73,29 +73,33 @@ def getKeys(self):
Data container class that keeps track of the setting files and the exposed sections.
"""
class SettingData:
- def __init__(self, xmlElement, basePath):
- self.file = xmlElement.get("file")
- self.name = xmlElement.get("name")
- if not os.path.isabs(self.file):
- self.file = basePath+"/"+self.file
- if not os.path.isfile(self.file):
- print "Warning: The file "+self.file+" referenced in "+self.name+" could not be found."
- print self.file
- self.sections = []
+ def __init__(self, xmlElement, basePath, id):
+ self._file = xmlElement.get("file")
+ self._name = xmlElement.get("name")
+ self._id = id
+ if not os.path.isabs(self._file):
+ self._file = basePath+"/"+self._file
+ if not os.path.isfile(self._file):
+ print "Warning: The file "+self._file+" referenced in "+self.name+" could not be found."
+ self._sections = []
for section in xmlElement.findall('Section'):
- self.sections.append(SectionData(section,0))
+ self._sections.append(SectionData(section,0))
+
+ @property
+ def id(self):
+ return self._id
- """
- @return: the file attribute of this setting data
- """
- def getSettingsFile(self):
- return self.file
+ @property
+ def settingsFile(self):
+ return self._file
- """
- @return: a list of all sections in this setting data
- """
- def getSections(self):
- return self.sections;
+ @property
+ def sections(self):
+ return self._sections
+
+ @property
+ def name(self):
+ return self._name
"""
@@ -105,44 +109,34 @@ class OptimizationSettingsManager:
def __init__(self, xmlFile):
tree = etree.parse(xmlFile)
root = tree.getroot()
- self.settingDatas = {}
+ self.settingDatas = []
self.currentContainer = None
self.currentConfig = None
- self.currentSetting = ""
+ self.currentSetting = None
basePath = os.path.dirname(xmlFile).replace("\\", "/")
+ settingID = 0
for settingData in root.findall('Setting'):
- if settingData.get("name") in self.settingDatas:
- print "Warning! The setting data "+settingData.get("name")+" is declared several times. Have you been sloppy-pasting?"
- self.settingDatas[settingData.get("name")] = SettingData(settingData, basePath)
+ self.settingDatas.append(SettingData(settingData, basePath,settingID))
+ settingID+=1
"""
- @return: a list of the names of all the available settings files
+ @return: the list of setting datas of all the available settings files
"""
- def getSettingNames(self):
- settingNames = []
- for key in self.settingDatas:
- settingNames.append(key)
- return settingNames
+ def getSettings(self):
+ return self.settingDatas
"""
- @return: the setting data of the setting matching the input name
+ @return: the setting data of the setting matching the input ID
"""
- def getSettingsFile(self, settingsName):
- return self.settingDatas[settingsName].getSettingsFile()
-
- """
- @return: the the list of sections from the setting data matching the name
- """
- def getSections(self, settingName):
- return self.settingDatas[settingName].getSections()
+ def getSetting(self, settingID):
+ return self.settingDatas[settingID]
"""
Enables/disables all the components in the optimization panel
@param enabled: true if the component should be enables
"""
def enable(self, enabled):
- sections = self.getSections(self.currentSetting)
- for section in sections:
+ for section in self.currentSetting.sections:
section.enable(enabled)
"""
@@ -151,26 +145,31 @@ def enable(self, enabled):
Creates the components for the exposed keys and adds them to the container.
Sets default values for the keys.
@param container: the container to place all UI elements in.
- @param selectedSettingName: the name of the newly selected setting.
+ @param selectedSettingID: the id of the newly selected setting.
"""
- def settingChanged(self, container, selectedSettingName):
+ def settingChanged(self, container, selectedSettingID):
+ settingData = self.getSetting(selectedSettingID)
# Load the configuration file
- self.currentConfig = self.loadConfigurationFile(self.getSettingsFile(selectedSettingName))
-
- #Clear the current interface
- if self.currentContainer != None:
- cmds.deleteUI(self.currentContainer)
-
+ self.currentConfig = self.loadConfigurationFile(settingData.settingsFile)
+ self.clear()
# Create the components that are exposed through the xml
- self.currentSetting = selectedSettingName
+ self.currentSetting = settingData
self.currentContainer = cmds.columnLayout (parent= container, adjustableColumn = True)
- if selectedSettingName != None:
- sections = self.getSections(selectedSettingName)
+ if selectedSettingID != None:
+ sections = settingData.sections
for section in sections:
section.createComponent(self.currentContainer)
self.setDefaultValues()
-
+
+ """
+ Removes all UI components from the interfaces
+ """
+ def clear(self):
+ #Clear the current interface
+ if self.currentContainer != None:
+ cmds.deleteUI(self.currentContainer)
+
"""
Loads the .ini file
@param configFile: the .ini file to load
@@ -188,8 +187,7 @@ def loadConfigurationFile(self, configFile):
"""
def writeTempConfig(self, outFile):
#Before we write the file we must transfer the user specified values to the config.
- sections = self.getSections(self.currentSetting)
- for section in sections:
+ for section in self.currentSetting.sections:
keys = section.getKeys()
for key in keys:
name = key.getKeyName()
@@ -198,15 +196,14 @@ def writeTempConfig(self, outFile):
value = key.getValue()
self.currentConfig.set(section, name,value)
else:
- print "Warning! The key: "+section+"/"+name+" does not exist in the config file! Check the xml description for the settings file: "+self.currentSetting
+ print "Warning! The key: "+section+"/"+name+" does not exist in the config file! Check the xml description for the settings file: "+self.currentSetting.name
self.currentConfig.write(outFile)
"""
Loops through all the keys in the current setting and sets the default values from the loaded config.
"""
def setDefaultValues(self):
- sections = self.getSections(self.currentSetting)
- for section in sections:
+ for section in self.currentSetting.sections:
keys = section.getKeys()
for key in keys:
name = key.getKeyName()
@@ -215,4 +212,4 @@ def setDefaultValues(self):
value = self.currentConfig.get(section, name)
key.setValue(value)
else:
- print "Warning! The key: "+section+"/"+name+" does not exist in the config file! Check the xml description for the settings file: "+self.currentSetting
\ No newline at end of file
+ print "Warning! The key: "+section+"/"+name+" does not exist in the config file! Check the xml description for the settings file: "+self.currentSetting.settingName
\ No newline at end of file
diff --git a/scripts/utils/__init__.py b/scripts/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scripts/utils/mutils.py b/scripts/utils/mutils.py
new file mode 100644
index 0000000..dde7788
--- /dev/null
+++ b/scripts/utils/mutils.py
@@ -0,0 +1,55 @@
+import maya.cmds as cmds
+from itertools import tee, izip
+
+"""
+Creates an iterable for a list that consists of tuples of 2.
+@param iterable: the list to make an iterable for
+"""
+def pairwise(iterable):
+ a = iter(iterable)
+ return izip(a, a)
+
+"""
+Returns the attribute component of the object.attribute string
+@param attrString: the attribute string
+"""
+def getAttribute(attrString):
+ attrIndex = attrString.rfind('.')
+ return attrString[attrIndex+1:]
+
+"""
+Returns a tuple of two elements, the first one is the object and the second one is the attribute.
+@attrString: string to splice
+"""
+def getObjectAndAttribute(attrString):
+ attrIndex = attrString.rfind('.')
+ return (attrString[:attrIndex],attrString[attrIndex+1:])
+
+"""
+Returns the attribute that is connected to the current attribute. Expecting there to be exactly one connection, otherwise
+an exception will be raised.
+@param attribute: the attribute to find the connection for
+@param isSource: true if the attribute is the source, false if it's the destination.
+@return: the connected attribute string
+"""
+def getConnectedAttribute(attribute, isSource):
+ if isSource:
+ connectedAttribute = cmds.connectionInfo(attribute,dfs=True)
+ else:
+ connectedAttribute = cmds.connectionInfo(attribute,sfd=True)
+ #If we're getting a list back we should verify that it's only connected to one attribute
+ if connectedAttribute != None and isinstance(connectedAttribute, list):
+ if len(connectedAttribute) > 1:
+ raise ValueError("Unexpectedly found several connections for the attribute: "+attribute )
+ connectedAttribute = connectedAttribute[0]
+ return connectedAttribute
+
+"""
+Moves the connection from the old attribute to the new attribute.
+@param oldSrcAttr: the previous source attribute
+@param newSrcAttr: the new source attribute
+@param targetAttr: the target attribute
+"""
+def moveConnection(oldSrcAttr, newSrcAttr, targetAttr):
+ cmds.disconnectAttr(oldSrcAttr, targetAttr)
+ cmds.connectAttr(newSrcAttr, targetAttr, f=True)
diff --git a/scripts/view/__init__.py b/scripts/view/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scripts/view/browsingpanel.py b/scripts/view/browsingpanel.py
new file mode 100644
index 0000000..87975de
--- /dev/null
+++ b/scripts/view/browsingpanel.py
@@ -0,0 +1,46 @@
+import maya.cmds as cmds
+import simplygonpanel
+reload(simplygonpanel)
+from simplygonpanel import SimplygonPanel
+
+CTRL_SETTINGSDIR = "SettingsDir"
+CTRL_BROWSEBUTTON = "OptButton"
+
+"""
+Wrapper for the browsing panel.
+"""
+class BrowsingPanel(SimplygonPanel):
+ def __init__(self, batchProcessor):
+ SimplygonPanel.__init__(self, "Browsing", batchProcessor)
+ self.defineControl(CTRL_SETTINGSDIR, "TextField")
+ self.defineControl(CTRL_BROWSEBUTTON, "Button")
+
+ """
+ Opens up a browser window that allows the user to specify where you can find the XML that describes the setting files to use
+ """
+ def onBrowse(self, _):
+ selectedSettings = cmds.fileDialog2(fm=1, fileFilter="XML Files (*.xml)", okc="Set")
+ if selectedSettings != None:
+ cmds.textField(self.getMObj(CTRL_SETTINGSDIR), edit=True, text=selectedSettings[0])
+ self._batchProcessor.setSettingsXML(selectedSettings[0])
+ print self._batchProcessor.settingsXML
+
+ """
+ Creates the panel containing the browsing component for the setting file xml and adds it to the main layout.
+ @param parentContainer: the container to add the panel to
+ """
+ def createPanel(self, parentContainer):
+ layout = cmds.rowLayout (parent= parentContainer, numberOfColumns = 2)
+ sd = cmds.textField(parent= layout, ed=False, w=400, text=self._batchProcessor.settingsXML)
+ self.setMObj(CTRL_SETTINGSDIR, sd)
+ bb = cmds.button(parent= layout, label = "Browse", command = self.onBrowse)
+ self.setMObj(CTRL_BROWSEBUTTON, bb)
+
+ """
+ Enables/disables browsing panel
+ @param enabled: true to enable all the components.
+ """
+ def enable(self, enabled):
+ #Only enable the color set selector if user weights are enabled
+ cmds.textField(self.getMObj(CTRL_SETTINGSDIR), edit=True, en=enabled)
+ cmds.button(self.getMObj(CTRL_BROWSEBUTTON), edit=True, en=enabled)
diff --git a/scripts/view/jobpanel.py b/scripts/view/jobpanel.py
new file mode 100644
index 0000000..9cdac24
--- /dev/null
+++ b/scripts/view/jobpanel.py
@@ -0,0 +1,94 @@
+import maya.cmds as cmds
+import simplygonpanel
+reload(simplygonpanel)
+from simplygonpanel import SimplygonPanel
+import manageoutputwindow
+reload(manageoutputwindow)
+from manageoutputwindow import *
+
+CTRL_AUTOCLEANUP = "AutoCleanUp"
+CTRL_TEXTUREDEST = "TextureDestination"
+CTRL_MANAGEOUTPUT = "ManageOuput"
+CTRL_BROWSE = "Browse"
+TEXTURE_DESTINATION_SETTING="SimplygonTextureDestination"
+JOB_AUTO_CLEAN="SimplygonJobAutoClean"
+
+"""
+Wrapper for the job management panel.
+"""
+class JobPanel(SimplygonPanel):
+ def __init__(self, batchProcessor):
+ SimplygonPanel.__init__(self, "Jobs", batchProcessor)
+ self.defineControl(CTRL_AUTOCLEANUP, "CheckBox")
+ self.defineControl(CTRL_TEXTUREDEST, "TextField")
+ self.defineControl(CTRL_MANAGEOUTPUT, "Button")
+ self.defineControl(CTRL_BROWSE, "Button")
+ """
+ Creates the panel containing the job management controls.
+ param parentContainer: the container to put the panel in
+ """
+ def createPanel(self, parentContainer):
+ layout = cmds.frameLayout(parent= parentContainer, l="Jobs", collapsable=False)
+ autoClean = False
+ if cmds.optionVar(exists= JOB_AUTO_CLEAN):
+ autoClean = cmds.optionVar(q=JOB_AUTO_CLEAN) == "True"
+ ac = cmds.checkBox(l="Auto Clean Up Jobs", value=autoClean, w=150, parent= layout, onc=self.onAutoClean, ofc=self.onAutoClean)
+ self.setMObj(CTRL_AUTOCLEANUP, ac)
+
+ browseLayout = cmds.rowLayout (parent= layout, numberOfColumns = 3)
+ # Fetch the texture destination folder from the environment.
+ textureDestination = ""
+ if cmds.optionVar(exists= TEXTURE_DESTINATION_SETTING):
+ textureDestination = cmds.optionVar(q=TEXTURE_DESTINATION_SETTING)
+
+ cmds.text(parent= browseLayout, label = "Texture destination:")
+ td = cmds.textField(parent= browseLayout, ed=False, w=400, text=textureDestination)
+ self.setMObj(CTRL_TEXTUREDEST, td)
+ self.setMObj(CTRL_BROWSE, cmds.button(parent= browseLayout, label = "Browse", command = self.onBrowse))
+ self.setMObj(CTRL_MANAGEOUTPUT, cmds.button(parent= layout, label = "Manage Output", command = self.onManageOutput))
+
+ """
+ Returns true if the user has selected to auto clean the job
+ """
+ @property
+ def autoClean(self):
+ return cmds.checkBox(self.getMObj(CTRL_AUTOCLEANUP), query = True, value=True)
+
+ """
+ Returns the texture directory
+ """
+ @property
+ def textureDir(self):
+ return cmds.textField(self.getMObj(CTRL_TEXTUREDEST), query=True, text=True)
+
+ """
+ Shows a window that allows the user to manage the list of jobs.
+ """
+ def onManageOutput(self, _):
+ window = ManageOutputWindow(self._batchProcessor)
+ window.showWindow()
+
+ """
+ Allows the user to set a directory to put all created textures in
+ """
+ def onBrowse(self, _):
+ textureDestination = cmds.fileDialog2(fm=3, okc="Set")[0]
+ if textureDestination != None:
+ cmds.textField(self.getMObj(CTRL_TEXTUREDEST), edit=True, text=textureDestination)
+ cmds.optionVar( sv=(TEXTURE_DESTINATION_SETTING, textureDestination) )
+
+ """
+ Called when the state of the auto clean checkbox changes to set the env variable
+ """
+ def onAutoClean(self, _):
+ cmds.optionVar( sv=(JOB_AUTO_CLEAN, cmds.checkBox(self.getMObj(CTRL_AUTOCLEANUP), query = True, value=True)))
+
+ """
+ Enable/disable the controls in the window
+ @param enabled: true if the controls should be enabled.
+ """
+ def enable(self, enabled):
+ cmds.checkBox(self.getMObj(CTRL_AUTOCLEANUP), edit=True, enable=enabled)
+ cmds.textField(self.getMObj(CTRL_TEXTUREDEST), edit=True, enable=enabled)
+ cmds.button(self.getMObj(CTRL_MANAGEOUTPUT), edit=True, enable=enabled)
+ cmds.button(self.getMObj(CTRL_BROWSE), edit=True, enable=enabled)
\ No newline at end of file
diff --git a/scripts/KeyModifierModule.py b/scripts/view/keymodifier.py
similarity index 100%
rename from scripts/KeyModifierModule.py
rename to scripts/view/keymodifier.py
diff --git a/scripts/view/manageoutputwindow.py b/scripts/view/manageoutputwindow.py
new file mode 100644
index 0000000..0265661
--- /dev/null
+++ b/scripts/view/manageoutputwindow.py
@@ -0,0 +1,176 @@
+import maya.cmds as cmds
+import simplygonpanel
+reload(simplygonpanel)
+from simplygonpanel import SimplygonPanel
+
+CTRL_VIEWCONTAINER = "ViewContainer"
+CTRL_JOBLIST = "JobList"
+CTRL_LODASSETS = "LODAssetContainer"
+MO_WINDOW_NAME = "SimplygonOutputManager"
+"""
+Window that allows the user to manage the output from jobs.
+"""
+class ManageOutputWindow(SimplygonPanel):
+ def __init__(self, batchProcessor):
+ SimplygonPanel.__init__(self, "ManageOutput", batchProcessor)
+ self.defineControl(CTRL_VIEWCONTAINER, "Container")
+ self.defineControl(CTRL_LODASSETS, "Container")
+ self.defineControl(CTRL_JOBLIST, "OptionMenu")
+ self._currentContainer = None
+
+ """
+ Create the job selection panel.
+ @param parentContainer: the container to add the components to
+ """
+ def createJobSelector(self, parentContainer):
+ layout = cmds.rowLayout(parent= parentContainer,numberOfColumns=2, adjustableColumn=2)
+ cmds.text(l="Jobs:")
+ jobList = cmds.optionMenu(parent= layout, cc=self.jobSelectionChanged)
+ self.setMObj(CTRL_JOBLIST, jobList)
+ jobIndex = 0
+ for job in self._batchProcessor.jobs:
+ cmds.menuItem(parent=jobList, label=job.name, data=jobIndex)
+ jobIndex+=1
+
+ """
+ Creates the panel where all the lod assets will be listed.
+ @param parentContainer: the container to add the panel to
+ """
+ def createLODListPanel(self, parentContainer):
+ scrollLayout = cmds.scrollLayout(parent= parentContainer, childResizable = True, horizontalScrollBarThickness=16, verticalScrollBarThickness=16)
+ self.setMObj(CTRL_LODASSETS, cmds.columnLayout(parent=scrollLayout, adjustableColumn=True, h=800))
+
+ """
+ Creates the panel with all the user action buttons.
+ @param parentContainer: the container to add the components to
+ """
+ def createActionPanel(self, parentContainer):
+ layout = cmds.columnLayout(parent= parentContainer, adjustableColumn=True)
+ cmds.button(parent= layout, label = "Clean job", command = self.onCleanJob)
+ cmds.button(parent= layout, label = "Split LODs", command = self.onSplit)
+ cmds.button(parent= layout, label = "Make Layers", command = self.onLayers)
+ cmds.button(parent= layout, label = "Move Textures", command = self.onMoveTextures)
+
+ """
+ Shows the manage output window
+ """
+ def showWindow(self):
+ self.updateJobList()
+ if cmds.window(MO_WINDOW_NAME, exists=True):
+ cmds.deleteUI(MO_WINDOW_NAME)
+
+ window = cmds.window(MO_WINDOW_NAME, title="Manage Output", iconName="DL", w=800, h=800)
+ mainLayout = cmds.columnLayout(parent= window, adjustableColumn=True)
+ self.createJobSelector(mainLayout)
+
+ subLayout = cmds.rowLayout(parent= mainLayout, numberOfColumns=2, adjustableColumn=1)
+ self.createLODListPanel(subLayout)
+
+ self.createActionPanel(subLayout)
+ #Force an update of the asset lists
+ self.jobSelectionChanged(None)
+ cmds.showWindow(window)
+
+ """
+ Adds the list of objects to the scroll list
+ @param scrollList: UI list to add the object names to
+ @param objNames: list of object names to add to the scroll list
+ """
+ def addObjects(self, scrollList, objNames):
+ for n in objNames:
+ cmds.textScrollList(scrollList, e=True, append = n)
+
+ """
+ Upates the interface
+ """
+ def refresh(self):
+ self.jobSelectionChanged(None)
+
+
+ """
+ Adds the assets that exists in the list of lods to the UI.
+ @param lods: list of assets to show
+ """
+ def showLODAssets(self, lods):
+ if self.getMObj(CTRL_VIEWCONTAINER) !=None:
+ cmds.deleteUI(self.getMObj(CTRL_VIEWCONTAINER))
+ container = cmds.columnLayout(parent=self.getMObj(CTRL_LODASSETS))
+ self.setMObj(CTRL_VIEWCONTAINER,container)
+ for lod in lods:
+ layout = cmds.frameLayout(parent= container, l="LOD"+str(lod), collapsable=True, font = "boldLabelFont")
+ listPanel = cmds.rowLayout(parent= layout, numberOfColumns=3, adjustableColumn1=True, adjustableColumn2=True, adjustableColumn3=True)
+ self.addObjects(cmds.textScrollList(parent= listPanel), set(lods[lod].objectNames))
+ self.addObjects(cmds.textScrollList(parent= listPanel), set(lods[lod].materialNames))
+ self.addObjects(cmds.textScrollList(parent= listPanel), set(lods[lod].textureNames))
+
+ """
+ Returns the currently selected job
+ """
+ def getSelectedJob(self):
+ selectedItemIndex = cmds.optionMenu(self.getMObj(CTRL_JOBLIST), query=True, select=True)-1
+ menuItems = cmds.optionMenu(self.getMObj(CTRL_JOBLIST), q=True, itemListLong=True)
+ selectedJobIndex = 0
+ if menuItems != None and (menuItems != [] or len(menuItems) < selectedItemIndex):
+ selectedJobIndex = cmds.menuItem(menuItems[selectedItemIndex], query=True, data=True)
+ else:
+ return None
+ return self._batchProcessor.jobs[selectedJobIndex]
+
+ """
+ Called whenever the job selection has changed
+ """
+ def jobSelectionChanged(self, _):
+ #Potentially dangerous if it removes the currently selected job
+ self.updateJobList()
+ job = self.getSelectedJob()
+ if job != None:
+ self.showLODAssets(job.getLODs())
+
+ """
+ Called when the user invokes the job cleaning
+ """
+ def onCleanJob(self, _):
+ self.updateJobList()
+ job = self.getSelectedJob()
+ if job != None:
+ job.pruneTexturesAndMaterials()
+ #We need to update the list of assets as the job will change it.
+ self.refresh()
+
+ """
+ Called when the user invokes splitting the lods
+ """
+ def onSplit(self, _):
+ self.updateJobList()
+ job = self.getSelectedJob()
+ if job != None:
+ job.splitLODs()
+
+ """
+ Called when the user invokes making layers
+ """
+ def onLayers(self, _):
+ self.updateJobList()
+ job = self.getSelectedJob()
+ if job != None:
+ job.makeLayers()
+
+ """
+ Called when the user invokes the action of moving assets
+ """
+ def onMoveTextures(self, _):
+ self.updateJobList()
+ job = self.getSelectedJob()
+ directory = cmds.fileDialog2(fm=3, okc="Move To")[0]
+ job.moveTextures(directory)
+
+
+ """
+ Loops through all jobs and cleans the up.
+ """
+ def updateJobList(self):
+ toRemove = []
+ for job in self._batchProcessor.jobs:
+ job.removeInvalidAssets()
+
+
diff --git a/scripts/view/optimizationpanel.py b/scripts/view/optimizationpanel.py
new file mode 100644
index 0000000..0819bf2
--- /dev/null
+++ b/scripts/view/optimizationpanel.py
@@ -0,0 +1,151 @@
+import maya.cmds as cmds
+import simplygonpanel
+reload(simplygonpanel)
+from simplygonpanel import SimplygonPanel
+import userweightpanel
+reload(userweightpanel)
+from userweightpanel import UserWeightsPanel
+
+CTRL_OPTCONTAINTER = "OptContainer"
+CTRL_OPTBUTTON = "OptButton"
+CTRL_SIMPLYGONBUTTON = "SimplygonButton"
+CTRL_SETTINGSSELECTOR = "SettingsSelector"
+
+"""
+Wrapper for the optimization setting panel.
+"""
+class OptimizationPanel(SimplygonPanel):
+ def __init__(self, batchProcessor):
+ SimplygonPanel.__init__(self, "OptimizationSettings", batchProcessor)
+ self._userWeightPanel = UserWeightsPanel(batchProcessor)
+ self._settingsManager = None
+ self.defineControl(CTRL_OPTCONTAINTER, "Container")
+ self.defineControl(CTRL_OPTBUTTON, "Button")
+ self.defineControl(CTRL_SIMPLYGONBUTTON, "Button")
+ self.defineControl(CTRL_SETTINGSSELECTOR, "OptionMenu")
+
+ """
+ Sets the manager of all the optimization settings. It's a bit messed up as it also holds some GUI logic. (TODO: Fix that!)
+ @param settingManager: the new settings manager
+ """
+ def setSettingsManager(self, settingManager):
+ self._settingsManager = settingManager
+ if self._settingsManager != None:
+ self.updateSettingFileList()
+
+
+ """
+ @return: true if the user weight checkbox is checked
+ """
+ @property
+ def useUserWeights(self):
+ return self._userWeightPanel.useUserWeights
+
+ """
+ @return: the integer value that the weight multiplier slider is set to
+ """
+ @property
+ def weightMultiplier(self):
+ return self._userWeightPanel.weightMultiplier
+
+ """
+ @return: the selected color set
+ """
+ @property
+ def colorSet(self):
+ return self._userWeightPanel.colorSet
+
+ """
+ Forwards the call to the user weights panel
+ """
+ def updateColorSets(self):
+ self._userWeightPanel.updateColorSets()
+
+ """
+ Creates the panel containing the setting drop list and add it to the main layout.
+ @param parentContainer: the container to add the panel to
+ """
+ def createSettingSelectorPanel(self, parentContainer):
+ layout = cmds.columnLayout (parent= parentContainer, adjustableColumn = True)
+ # Create the header
+ cmds.text(parent= layout, l="Optimization settings", align="center", font="boldLabelFont")
+ cmds.separator(parent= layout, height=20, style="doubleDash")
+
+ #Add the settings browser component
+ ss = cmds.optionMenu(parent= layout, cc=self.settingChanged)
+ self.setMObj(CTRL_SETTINGSSELECTOR, ss)
+ if self._settingsManager != None:
+ self.updateSettingFileList()
+ cmds.separator(parent= layout, height=20, style="none")
+
+ """
+ Creates the main window
+ @param parentContainer: the container to add the panel to
+ """
+ def createPanel(self, parentContainer):
+ # Add the setting selector panel
+ self.createSettingSelectorPanel(parentContainer)
+ oc = cmds.frameLayout(parent= parentContainer, borderStyle = 'etchedOut', borderVisible=True, lv =False)
+ self.setMObj(CTRL_OPTCONTAINTER,oc)
+ self._userWeightPanel.createPanel(parentContainer)
+ endLayout = cmds.rowLayout(numberOfColumns=2, parent= parentContainer)
+ ob = cmds.button(parent= endLayout, label="Optimize", c=self.onOptimize, w=250)
+ self.setMObj(CTRL_OPTBUTTON,ob)
+ sb = cmds.button(parent= endLayout, label="Send to Simplygon", c=self.onSimplygon, w=250)
+ self.setMObj(CTRL_SIMPLYGONBUTTON,sb)
+
+ """
+ Starts a Simplygon optimization in batch mode with the currently selected settings.
+ """
+ def onOptimize(self, _):
+ self._batchProcessor.startSimplygon(True)
+
+ """
+ Starts the Simplygon GUI with the currently selected settings and selected objects.
+ """
+ def onSimplygon(self, _):
+ self._batchProcessor.startSimplygon(False)
+
+
+ """
+ Will update the settings components whenever the selected setting has been changed.
+ """
+ def settingChanged(self, *args):
+ settingSelector = self.getMObj(CTRL_SETTINGSSELECTOR)
+ selectedItemIndex = cmds.optionMenu(settingSelector, query=True, select=True)-1
+ menuItems = cmds.optionMenu(settingSelector, q=True, itemListLong=True)
+ selectedSettingID = 0
+ if menuItems != None and (menuItems != [] or len(menuItems) < selectedItemIndex):
+ selectedSettingID = cmds.menuItem(menuItems[selectedItemIndex], query=True, data=True)
+
+ if self._settingsManager != None:
+ self._settingsManager.settingChanged(self.getMObj(CTRL_OPTCONTAINTER), selectedSettingID)
+ self._batchProcessor.refreshViews()
+
+ """
+ Refreshes the settings drop list based of the current settings in the settings manager.
+ """
+ def updateSettingFileList(self):
+ # Delete the current set of settings
+ settingSelector = self.getMObj(CTRL_SETTINGSSELECTOR)
+ try:
+ menuItems = cmds.optionMenu(settingSelector, q=True, itemListLong=True)
+ if menuItems != None and menuItems != []:
+ cmds.deleteUI(menuItems)
+ except:
+ pass
+ settings = self._settingsManager.getSettings()
+ for s in settings:
+ cmds.menuItem(parent=settingSelector, label=s.name, data=s.id)
+ self._settingsManager.settingChanged(self.getMObj(CTRL_OPTCONTAINTER), 0)
+
+ """
+ Enable/disable the controls in the window
+ @param enabled: true if the controls should be enabled.
+ """
+ def enable(self, enabled):
+ cmds.button(self.getMObj(CTRL_OPTBUTTON), edit=True, en=enabled)
+ cmds.button(self.getMObj(CTRL_SIMPLYGONBUTTON), edit=True, en=enabled)
+ if self._settingsManager != None:
+ self._settingsManager.enable(enabled)
+ self._userWeightPanel.enable(enabled)
\ No newline at end of file
diff --git a/scripts/view/simplygonpanel.py b/scripts/view/simplygonpanel.py
new file mode 100644
index 0000000..f9fe283
--- /dev/null
+++ b/scripts/view/simplygonpanel.py
@@ -0,0 +1,121 @@
+import maya.cmds as cmds
+
+"""
+Wrapper class to keep track of controls that are used in the SimplygonPanels.
+"""
+class SimplygonControl:
+ def __init__(self, name, type):
+ self._name = name
+ self._type = type
+ self._mObj = None
+
+ """
+ Returns the name (not maya name) of this component.
+ """
+ @property
+ def name(self):
+ return self._name
+
+ """
+ Returns the type (not necessarily compatible with maya) of this component.
+ """
+ @property
+ def type(self):
+ return self._type
+
+ """
+ Returns the Maya object corresponding to this control.
+ """
+ @property
+ def mObj(self):
+ return self._mObj
+
+ """
+ Sets the Maya object corresponding to this control.
+ """
+ @mObj.setter
+ def mObj(self, mObj):
+ self._mObj = mObj
+
+"""
+Parent class for panels that are used in the SimplygonBatchProcessor
+"""
+class SimplygonPanel:
+ def __init__(self, name, batchProcessor):
+ self._name = name
+ self._batchProcessor = batchProcessor
+ self._controls = {}
+
+ """
+ Returns the name of this panel.
+ """
+ @property
+ def name(self):
+ return self._name
+
+ """
+ Returns the batch processor
+ """
+ @property
+ def batchProcessor(self):
+ return self._batchProcessor
+
+ """
+ Defines a control that will exist in the context of this panel.
+ @param name: Name of the control, must be unique within this panel
+ @param type: Type of control, might come in handy at some point
+ """
+ def defineControl(self, name, type):
+ if name in self._controls:
+ raise NameError("Error: The control "+name+" has already been defined in the panel "+self.name)
+ self._controls[name] = SimplygonControl(name, type)
+
+ """
+ Clears the controls in this panel. Also deleting any MayaObj's from the UI.
+ """
+ def clearControls(self):
+ for name, control in self._controls.iteritems():
+ if control.mObj != None:
+ cmds.deleteUI(control.mObj)
+ self._controls = {}
+
+ """
+ Returns the control with the corresponding name, None if the control does not exist.
+ @param name: Name of the control to get
+ """
+ def getControl(self, name):
+ return self._controls[name]
+
+ """
+ Returns the maya object of the control with the corresponding name, None if the control does not exist.
+ @param name: Name of the control to get
+ """
+ def getMObj(self, name):
+ control = self.getControl(name)
+ if control == None:
+ raise NameError("Cannot return the maya object for "+name+". It doesn't exist.")
+ return control.mObj
+
+ """
+ Sets the maya object of the control with the corresponding name, if the control does not exist a NameError will be thrown.
+ @param name: Name of the control to get
+ """
+ def setMObj(self, name, mObj):
+ control = self.getControl(name)
+ if control == None:
+ raise NameError("Cannot set the maya object for component "+name+". It doesn't exist.")
+ control.mObj = mObj
+
+
+ """
+ --------------------------------------------------------------------------------------
+ INTERFACE FUNCTIONS
+ --------------------------------------------------------------------------------------
+ """
+
+ """
+ Should be implemented by all classes
+ @param parentContainer: the container to add the components to
+ """
+ def createPanel(self, parentContainer):
+ raise NotImplementedError("createPanel is not implemented on the panel: "+self.name)
\ No newline at end of file
diff --git a/scripts/view/userweightpanel.py b/scripts/view/userweightpanel.py
new file mode 100644
index 0000000..2333209
--- /dev/null
+++ b/scripts/view/userweightpanel.py
@@ -0,0 +1,96 @@
+import maya.cmds as cmds
+import simplygonpanel
+reload(simplygonpanel)
+from simplygonpanel import SimplygonPanel
+
+CTRL_USERWEIGHTS = "UseUserWeigths"
+CTRL_COLORSETS = "ColorSets"
+CTRL_WEIGHTMULTIPLIER = "WeightMultiplier"
+
+"""
+Wrapper for the user weight data panel.
+"""
+class UserWeightsPanel(SimplygonPanel):
+ def __init__(self, batchProcessor):
+ SimplygonPanel.__init__(self, "UserWeights", batchProcessor)
+ self.defineControl(CTRL_USERWEIGHTS, "CheckBox")
+ self.defineControl(CTRL_COLORSETS, "OptionsMenu")
+ self.defineControl(CTRL_WEIGHTMULTIPLIER, "IntSlider")
+
+ """
+ Creates the panel that contains the user weights settings
+ @param parentContainer: the container to add the user weight panel to
+ """
+ def createPanel(self, parentContainer):
+ # Add the user weights components
+ layout = cmds.frameLayout(parent= parentContainer, l="User weights", collapsable=True, collapse=True, font = "boldLabelFont")
+ cmds.separator(parent= layout, height=1, style="none")
+ weightsLayout = cmds.rowLayout (parent= layout, numberOfColumns = 2)
+ cb = cmds.checkBox(l="Enable", w=150, parent= weightsLayout, onc=self.updateColorSets, ofc=self.updateColorSets)
+ self.setMObj(CTRL_USERWEIGHTS, cb)
+ cls = cmds.optionMenu(parent= weightsLayout, w=350, en=False)
+ self.setMObj(CTRL_COLORSETS, cls)
+ cmds.separator(parent= layout, height=1, style="none")
+ weightsMulLayout = cmds.rowLayout (parent= layout, numberOfColumns = 2)
+ cmds.text(parent= weightsMulLayout, l="Weights multiplier", align="right", w=150)
+ wm = cmds.intSlider(min=1, max=8, value=1, step=1, parent= weightsMulLayout, w=350)
+ self.setMObj(CTRL_WEIGHTMULTIPLIER, wm)
+ cmds.separator(parent= layout, height=1, style="none")
+
+ """
+ @return: true if the user weight checkbox is checked
+ """
+ @property
+ def useUserWeights(self):
+ return cmds.checkBox(self.getMObj(CTRL_USERWEIGHTS), query = True, value=True)
+
+ """
+ @return: the integer value that the weight multiplier slider is set to
+ """
+ @property
+ def weightMultiplier(self):
+ return cmds.intSlider(self.getMObj(CTRL_WEIGHTMULTIPLIER), query=True, value=True)
+
+ """
+ @return: the selected color set
+ """
+ @property
+ def colorSet(self):
+ return cmds.optionMenu(self.getMObj(CTRL_COLORSETS), query=True, value=True)
+
+
+ """
+ Should be called every time the color set selector needs to be updated. Will remove the current
+ set of options and fetch the current possible sets and add them to the droplist
+ """
+ def updateColorSets(self, *args):
+ # Delete the current set of color sets
+ csSelector = self.getMObj(CTRL_COLORSETS)
+ try:
+ menuItems = cmds.optionMenu(csSelector, q=True, itemListLong=True)
+ if menuItems != None and menuItems != []:
+ cmds.deleteUI(menuItems)
+ except:
+ pass
+ colorSets = cmds.polyColorSet( query=True, allColorSets=True)
+ if colorSets :
+ colorSets = list(set(colorSets))
+ for c in colorSets:
+ cmds.menuItem(parent=csSelector, label=c)
+ if self.useUserWeights:
+ cmds.optionMenu(csSelector, edit=True, en=True)
+ cmds.intSlider(self.getMObj(CTRL_WEIGHTMULTIPLIER), edit=True, en=True)
+ else:
+ cmds.optionMenu(csSelector, edit=True, en=False)
+ cmds.intSlider(self.getMObj(CTRL_WEIGHTMULTIPLIER), edit=True, en=False)
+
+
+ """
+ Enables/disables the user weight selection
+ @param enabled: true to enable all the components.
+ """
+ def enable(self, enabled):
+ #Only enable the color set selector if user weights are enabled
+ cmds.checkBox(self.getMObj(CTRL_USERWEIGHTS), edit=True, en=enabled)
+ cmds.optionMenu(self.getMObj(CTRL_COLORSETS), edit=True, en=enabled and self.useUserWeights)
+ cmds.intSlider(self.getMObj(CTRL_WEIGHTMULTIPLIER), edit=True, en=enabled and self.useUserWeights)
diff --git a/settings/Settings.xml b/settings/Settings.xml
index 4eeafb5..e56ee21 100644
--- a/settings/Settings.xml
+++ b/settings/Settings.xml
@@ -58,7 +58,8 @@
-