Maxscript: Arrange a list in descending order of material ID's assigned to an object

Hey there,

It’s my first post on this site :slight_smile: I have a question/in need of some help. I have been wracking my brain trying to figure this one out for a while now.

So I have this tool that lists the stats of objects in the scene. The last column of my tool lists the number of materials you have assigned to the faces of each object. This works fine, however…the way in which I have done it does not allow me to sort them properly.

So how have I done it? Well, first I gather the objects in the scene into an array called mySelectedObjects, then I loop through each object selecting faces based on material ID’s, if the number of faces selected is more than 0 then the material count is increased, if it is 0 then it is not. I do this for however many sub materials are in the multi-material (if any) then I iterate through the two arrays and place them in the listview.

The problem comes when I try to sort by material ID. Sorting by verts, faces etc. is easily done because I just pinch the stats using object.verts.count and then rearrange it that way. But because the material count array is just a set of numbers, I am running into problems. I can qsort the numbers array but I need to be able to sort the objects array with it.

The way I am doing it at the moment is SO SO SO SLOW and isn’t really working for me…I am sorting the material array, then looping through it and then looping throuigh the objects array, counting the materials assigned and if it matches then put it in a temporary array in order and then use that to popuylate the list.

But it is getting very confusing and very slow. There must be an easier way!! Anyway here is the function for this:

Any help/tips would be greatly appreciated, this is the last thing I have to do before the tool is finished and I can release it!

fn sortByMaterial =
(
--Sorts through the (now sorted) master material array and matches objects from the MSO array to the value
	-- If a match is found then the object is placed into a new temporary array
	-- The object is removed from the MSO array so that duplicates are not placed into the new temp array
	-- The MSO array is then remade using the order from the tempArray
	
	local arrayTrack = 1 -- variable to hold the index number of the MSO array object
	matId = 1
	matNum = 0
	
	count = masterMatArray.count
	
	for i = 1 to count do
	(
		index = masterMatArray[i]
		
		for j in mySelectedObjects do
		(
			mat = j.material
			
			if(classof j !=  Editable_Poly and classof j != Editable_Mesh and classof  j != Editable_Patch) then
			(
				if(mat != undefined) then
				(
					matNum = 1
				)
				else 
				(
					matNum = 0
				)
				
				arrayTrack += 1
			)
			else
			(						
				if(classof mat != MultiMaterial) then
				(
					if(mat != undefined) then
					(
						matNum = 1
					)
					else
					(
						matNum = 0
					)
				)
				else
				(
					numberOfSubs = mat.numsubs
					
					if(classOf j == Editable_Mesh or classOf j == Editable_Patch) then
					(
-- 						print("Converted " + i as string)
						copyOfObj = copy j
						convertToPoly copyOfObj
						j = copyOfObj
						isCopied = true
					)
					
					for k = 1 to numberOfSubs do
					(
						j.selectbymaterial matID
							
						faceArr = getFaceSelection j
						--print("face array count is: " + faceArr.numberset as string)
						
						if(faceArr.numberset != 0) then
						(
							matNum += 1 
						)
						matID += 1
					)
				)
				deselect j.faces
				arrayTrack += 1
			)
			
			if(matNum == index) then
			(
				append tempArray j
			)
			matNum = 0
			matID = 1
			
			if(isCopied == true) then
			(
				delete j
				isCopied = false
			)
		)
	)
	
	mySelectedObjects = #()
	for l in tempArray do
	(
		append mySelectedObjects l
	)
	tempArray = #()
)

I fixed it in the end. I even got to completely remove the function. In case anyone else has a similar issue to this what I did was use setUserProp and assigned the material count to the user defined property. I then read from that to populate the list etc. It worked out a lot fast and cleaner than what I was doing before which in retrospect was horribly messy :\

Out of curiosity, how often are you changing/checking the values? I’d just stick with a refresh button and once the contents of the list are there, sort just that list. I’ve included a very barebones version of ListViewItemComparer for that purpose just to illustrate the approach - apart from the c (for column) variable you could also add a sorting order and sorting type ones instead of this hardcoded check if it’s the second column or not :wink:

try destroyDialog test catch()
rollout test "" width:225
(
	dotNetControl dncObjList "System.Windows.Forms.ListView" width:200 height:200
	button btnFill "Fill It" width:200 height:25

	local dnListItem = dotNetClass "System.Windows.Forms.ListViewItem"

	fn compileListItemSorter =
	(
		local source = "using System;
"
		source += "using System.Windows.Forms;
"
		source += "using System.Collections;
"
		source += "class ListViewItemComparer : IComparer
"
		source += "{
"
		source += "	private int c;
"
		source += "	public ListViewItemComparer() { c = 0; }
"
		source += "	public ListViewItemComparer(int col) { c = col; }
"
		source += "	public int Compare(object x, object y)
"
		source += "	{
"
		source += "		if (c == 1) return Convert.ToInt32(((ListViewItem)x).SubItems[c].Text).CompareTo(
"
		source += "			Convert.ToInt32(((ListViewItem)y).SubItems[c].Text));
"
		source += "		else return String.Compare(((ListViewItem)x).SubItems[c].Text,
"
		source += "			((ListViewItem)y).SubItems[c].Text);
"
		source += "	}
"
		source += "}"

		local csharpProvider = dotNetObject "Microsoft.CSharp.CSharpCodeProvider"
		local compilerParams = dotNetObject "System.CodeDom.Compiler.CompilerParameters"
		compilerParams.GenerateInMemory = true
		compilerParams.ReferencedAssemblies.Add("System.Windows.Forms.dll")
		compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source)
		compilerResults.CompiledAssembly
	)

	fn getIDCount obj ids:#{} =
	(
		local mesh = obj.mesh
		local faces = mesh.faces as bitarray

		for face in faces do
			append ids (getFaceMatID mesh face)

		delete mesh
		ids.numberSet
	)

	fn getItem obj channelCount =
	(
		local item = dotNetObject dnListItem obj
		item.SubItems.Add (channelCount as string)
		item
	)

	fn getItems objs =
		for obj in objs collect
			case classOf obj.material of
			(
				MultiMaterial : getItem obj.name (getIDCount obj)
				UndefinedClass : getItem obj.name 0
				default : getItem obj.name 1
			)

	on test open do
	(
		dncObjList.View = dncObjList.View.Details
		dncObjList.BorderStyle = dncObjList.BorderStyle.FixedSingle
		dncObjList.FullRowSelect = true
		dncObjList.GridLines = true

		dncObjList.Columns.Add "Object Name" 100
		dncObjList.Columns.Add "ID Channels" 100

		compileListItemSorter()
	)

	on dncObjList columnClick columnHeader do
	(
		dncObjList.ListViewItemSorter = dotNetObject "ListViewItemComparer" columnHeader.Column
		dncObjList.ListViewItemSorter = undefined
	)

	on btnFill pressed do
	(
		dncObjList.Items.Clear()
		dncObjList.Items.AddRange (getItems selection)
	)
)
createDialog test

I am only changing/checking the values if the user presses the refresh button :stuck_out_tongue: or if the user selects something in the list that they have deleted then it automatically refreshes it. After I fixed the material ID count issue I changed a few things so that as your advice suggests just change the list as I already have the values. It makes it much faster than it was previously :slight_smile:

Just a few bugs to fix then its release time! I will post up once I have released it and tidied up the code, I am hoping people will find it quite useful :slight_smile: