Welcome to WSL!

Make yourself at home, but before posting, please may I ask you to read the following topics.


Posting 101
Server space, screenshots, and you

Thank you!

PS. please pretty please:


Image

Building a Version Control script — collaborative learning

User avatar
AndrewHazelden
Fusionator
Posts: 1648
Joined: Fri Apr 03, 2015 3:20 pm
Answers: 9
Location: West Dover, Nova Scotia, Canada
Been thanked: 21 times
Contact:

Re: Building a Version Control script — collaborative learning

#16

Post by AndrewHazelden » Thu Feb 08, 2018 1:58 pm

Hi.

As a quick comment, I'm pretty sure if this code is present in the final version of your tool you are making the script not able to function on Fusion (Free):

Code: Select all

-- Connect to Fusion on the local system
hostname = "localhost"
fusion = bmd.scriptapp("Fusion", hostname)

User avatar
Midgardsormr
Fusionator
Posts: 1823
Joined: Wed Nov 26, 2014 8:04 pm
Answers: 15
Location: Los Angeles, CA, USA
Been thanked: 112 times
Contact:

Re: Building a Version Control script — collaborative learning

#17

Post by Midgardsormr » Thu Feb 08, 2018 3:00 pm

Thanks for the heads up; I'll add a test for remote control. If the script is being run from within Fusion, that routine will not execute:

  1. if fusion == nil then
  2.     -- Connect to Fusion on the local system
  3.     hostname = "localhost"
  4.     fusion = bmd.scriptapp("Fusion", hostname)
  5.  
  6.     -- Set a quick shortcut to the Fusion object
  7.     fu = fusion
  8.  
  9.     -- Test to be sure Fusion is running and print a confirmation to the console.
  10.     if fusion ~= nil then
  11.         c = fusion.CurrentComp  -- Set a shortcut to the composition object.
  12.         SetActiveComp(comp)    
  13.         c:Print("[bmd.scriptapp] Connected to Fusion on \"" .. hostname .. "\"\n\n")
  14.     else
  15.         c:Print("[bmd.scriptapp] Error: Please open the Fusion GUI before running this tool.\n\n")
  16.     end
  17. else
  18.     composition:Print("Local control.")
  19.     fu = fusion
  20.     c = composition
  21. end
edit: And while I'm at it, that entire thing could just as well be a function. I'll call it setEnvirons()

User avatar
AndrewHazelden
Fusionator
Posts: 1648
Joined: Fri Apr 03, 2015 3:20 pm
Answers: 9
Location: West Dover, Nova Scotia, Canada
Been thanked: 21 times
Contact:

Re: Building a Version Control script — collaborative learning

#18

Post by AndrewHazelden » Thu Feb 08, 2018 3:12 pm

With the following line of code you will typically want to add a newline \n to the end of the string to mimic the output you would get with a regular "print()" function:

Code: Select all

composition:Print("Local control.")

User avatar
AndrewHazelden
Fusionator
Posts: 1648
Joined: Fri Apr 03, 2015 3:20 pm
Answers: 9
Location: West Dover, Nova Scotia, Canada
Been thanked: 21 times
Contact:

Re: Building a Version Control script — collaborative learning

#19

Post by AndrewHazelden » Thu Feb 08, 2018 3:44 pm

You might want to look over the logic of this line of code as you are defining the variable "c" with the value for the current comp pointer, then expecting an alternative variable "comp" to pre-exist and setting that as the active comp. If c is what you want to use for the current comp, maybe you want to also use that for setting the active comp on the next line of code for consistency? :)

This is the code I am discussing:

Code: Select all

c = fusion.CurrentComp  -- Set a shortcut to the composition object.
SetActiveComp(comp)
If "composition" already exists, you now have three identical variables for the same comp pointer as logically c == comp == composition. If you ran the following command in the Console you should compare the output to see which are initialized at this point in the code when you connect to the Fusion process from FuScript:

Code: Select all

dump(c)
dump(comp)
dump(composition)
By that same reasoning there is no functional difference/reason IMHO to alternate between using these two different comp pointer based print commands in the same script:

Code: Select all

composition:Print()
c:Print()
You could just as easily use this type of code to populate several identical variables with the same pointer value for convenience: :)

Code: Select all

composition = fu.CurrentComp
comp = composition
c = composition
SetActiveComp(composition)

User avatar
Midgardsormr
Fusionator
Posts: 1823
Joined: Wed Nov 26, 2014 8:04 pm
Answers: 15
Location: Los Angeles, CA, USA
Been thanked: 112 times
Contact:

Re: Building a Version Control script — collaborative learning

#20

Post by Midgardsormr » Thu Feb 08, 2018 4:30 pm

That's what ya get when you copy-paste from other scripts!

I'd put in the line printing "Local control" before I realized that I wouldn't have the shortcut variables. So then I added those… underneath the print statement. I saw what I'd done a couple minutes later, but I hadn't bothered changing it in my post here. I doubt that print statement will even be in the final script—there's not really much reason to inform the user that they ran the script from within Fusion. They know that.

User avatar
AndrewHazelden
Fusionator
Posts: 1648
Joined: Fri Apr 03, 2015 3:20 pm
Answers: 9
Location: West Dover, Nova Scotia, Canada
Been thanked: 21 times
Contact:

Re: Building a Version Control script — collaborative learning

#21

Post by AndrewHazelden » Thu Feb 08, 2018 4:38 pm

Midgardsormr wrote:
Fri Feb 02, 2018 4:53 pm
The next thing is to select a programmer's text editor. Normally I like to use Notepad++, but for this project I am going to use Sublime because it has a feature that will allow me to interface with Fusion's script interpreter directly from the editor. I am definitely not a power user, so I don't know everything that can be done with Sublime's Build Systems. This example is fairly primitive.

A Build System in Sublime is used to invoke a compiler or script interpreter and give it your current file to execute. Since Fusion Studio permits external control of a session, I can use the Build System to run my script without ever having to remove my focus from the editor. Many thanks to @Sbenjamin for making me aware that I could do that, and to @AndrewHazelden for giving me some more sophisticated code to use in making the connection.
Also, I'd like to mention that you don't have to leave Notepad++ for what you are doing if you don't want to.

You can add the free NppExec plugin to a stock version of Notepad++ using the Notepad++ plugin manager.
Notepad++ Plugin Manager.png
Then NppExec can be used to launch custom external tools (such as FuScript) with your own variables, the current document name or line selection, or other parameters from your Notepad++ editing session. After you start using NppExec you can add your FuScript customizations such as a Notepad++ menu entry for your scripts, and assign hotkey bindings too.

NppExec adds has a local help text file that is readable once you install it for usage details.

This Stack Overflow post will give you some ideas on how NppExec can be used:
https://stackoverflow.com/questions/250 ... xec-plugin
You do not have the required permissions to view the files attached to this post.

User avatar
AndrewHazelden
Fusionator
Posts: 1648
Joined: Fri Apr 03, 2015 3:20 pm
Answers: 9
Location: West Dover, Nova Scotia, Canada
Been thanked: 21 times
Contact:

Re: Building a Version Control script — collaborative learning

#22

Post by AndrewHazelden » Thu Feb 08, 2018 7:19 pm

Midgardsormr wrote:
Thu Feb 08, 2018 1:36 pm
In order to test my logic, I put all of this new code into Sublime and attempted to execute it remotely. I ran into a problem: when executed from outside, FuScript apparently doesn't know about the bmd.scriptlib. bmd.split() doesn't work! Okay, I can think of a couple of ways to get around this:
  • I can use an environment variable in the operating system to provide a path to the scriptlib. This would have the advantage of giving me a place to put other Lua libraries, and I could access any of them from my Fusion scripts. But I'd have to build in a mechanism to assist the user in setting up the variable and the library repository. That's probably a no-go for most users.
  • ...
  • I can try to query the pathmap to the Scripts directory and reverse it to get the location of bmd.scriptlib. The problems with that are a) I have no idea how to do it, although I'm sure I could find a code sample that would teach me, and b) it seems overly complex in comparison to option 3.
You could try using Lua code like this to locate and source the scriptlib file so its functions are accessible inside of a FuScript terminal session:

Code: Select all

ldofile(fu:MapPath("Scripts:/bmd.scriptlib"))
My guess is your scriptlib sourcing issues are possibly related to having Fu 9.0.1 still being installed on one of your systems as some changes were made in v9.0.2 to make the scriptlib loading work more reliably the before from the Fusion Console tab/Script menu. :)

If you are going to clone some of the functions from the bmd.scriptlib file and update them, here is a revised pathIsMovieFormat() function I used inside of the Archive Composition script that includes entries for the new movie formats added in Fusion 9.

Code: Select all

function pathIsMovieFormat(path)
	local extension = bmd.getextension(path):lower()
	if extension ~= nil then
		if ( extension == "3gp" ) or
				( extension == "aac" ) or
				( extension == "aif" ) or
				( extension == "aiff" ) or
				( extension == "avi" ) or
				( extension == "dvs" ) or
				( extension == "fb"  ) or
				( extension == "flv" ) or
				( extension == "m2ts" ) or
				( extension == "m4a" ) or
				( extension == "m4b" ) or
				( extension == "m4p" ) or
				( extension == "mkv" ) or
				( extension == "mov" ) or
				( extension == "mp3" ) or
				( extension == "mp4" ) or
				( extension == "mts" ) or
				( extension == "mxf" ) or
				( extension == "omf" ) or
				( extension == "omfi" ) or
				( extension == "qt" ) or
				( extension == "stm" ) or
				( extension == "tar" ) or
				( extension == "vdr" ) or
				( extension == "vpv" ) or
				( extension == "wav" ) or
				( extension == "webm" ) then
			return true
		end
	end
	return false
end
I also added a new audio file checking function to the Archive Composition script too and that code might be of some use if you ever end up needing to check the file extensions of all the media types loaded in a Loader/Saver node (SoundFilename)/Fusion timeline (COMPS_AudioFilename)/SuckLessAudio Modifier fuse (Fuse.SuckLessAudio - WaveFile[0]):

Code: Select all

function pathIsAudioFormat(path)
	local extension = bmd.getextension(path):lower()
	if extension ~= nil then
		if  ( extension == "aac" ) or
				( extension == "aif" ) or
				( extension == "aiff" ) or
				( extension == "m4a" ) or
				( extension == "mp3" ) or
				( extension == "wav" ) then
			return true
		end
	end
	return false
end

User avatar
Midgardsormr
Fusionator
Posts: 1823
Joined: Wed Nov 26, 2014 8:04 pm
Answers: 15
Location: Los Angeles, CA, USA
Been thanked: 112 times
Contact:

Re: Building a Version Control script — collaborative learning

#23

Post by Midgardsormr » Fri Feb 09, 2018 1:06 pm

Thanks for the pointers! I had forgotten that Notepad++ can take plug-ins.
AndrewHazelden wrote:
Thu Feb 08, 2018 7:19 pm
My guess is your scriptlib sourcing issues are possibly related to having Fu 9.0.1 still being installed on one of your systems as some changes were made in v9.0.2 to make the scriptlib loading work more reliably the before from the Fusion Console tab/Script menu. :)
I don't have any difficulty reaching the scriptlib from within Fusion, only when running the script externally. Long-range goals might involve a connection to a project and asset management system, so I'm trying to avoid painting myself into a corner.

Looks like your suggested line works like a charm from Sublime, so there's option 4!

User avatar
Midgardsormr
Fusionator
Posts: 1823
Joined: Wed Nov 26, 2014 8:04 pm
Answers: 15
Location: Los Angeles, CA, USA
Been thanked: 112 times
Contact:

Re: Building a Version Control script — collaborative learning

#24

Post by Midgardsormr » Mon Feb 12, 2018 5:41 pm

At this point, we've completely solved the case where the user selected just one Loader. We get its path up to and including the version folder, compare all the other Loaders' paths to it, and remove any Loaders that don't match from our list.

The two other cases force us to ask for user intervention—we have more than one possible pattern, and we need to ask the user which set of Loaders they wish to update. When I first began designing the script, I assumed that we would update only one Loader set at a time, but the more I think about it, the more I think it would be really cool if we could manage all of our Loader versions at once in a single panel. Andrew's provided a prototype in the UI Manager examples called Media Tree that I think we can mine for ideas. If you haven't already done so, use Reactor to install UI Manager Lua Examples. If you open your Reactor folder with Reactor > Advanced > Show Reactor Folder, the examples are in /Deploy/Scripts/Comp/UI Manager/

I'm going to spend this lesson on wireframing an interface, the design of which will inform the next step in writing the script. I'll start by opening up the UI Manager example My First Window.lua. This is the most bare-bones example of opening a UI Manager window, and I find it useful as a starting point for any new interface.

As I mentioned in the above reply to Andrew, I've turned the code that connects Sublime and Fusion into a function that also tests whether or not we are, in fact, operating remotely. I have added that function and a call to it to the top of the example and tested it to confirm that a window appears. Here's where we're starting:

Code: [Select all] [Expand/Collapse] [Download] (My First Window.lua)
  1. ----------------------------------------------------
  2. -- setEnvirons()
  3. --
  4. -- Creates a connection to Fusion if the script is
  5. -- run from an external process. Creates shortcut
  6. -- variables c and fu for composition and fusion,
  7. -- respectively.
  8. ----------------------------------------------------
  9. function setEnvirons()
  10.     if fusion == nil then           -- We're operating remotely.
  11.         -- Connect to Fusion on the local system
  12.         hostname = "localhost"
  13.         fusion = bmd.scriptapp("Fusion", hostname)
  14.  
  15.         -- Set a quick shortcut to the Fusion object
  16.         fu = fusion
  17.  
  18.         -- Test to be sure Fusion is running and print a confirmation to the console.
  19.         if fusion ~= nil then
  20.             c = fusion.CurrentComp  -- Set a shortcut to the composition object.
  21.             SetActiveComp(c)
  22.            
  23.             ldofile(fu:MapPath("Scripts:/bmd.scriptlib"))       -- Load the bmd library
  24.             c:Print("[bmd.scriptapp] Connected to Fusion on \"" .. hostname .. "\"\n\n")
  25.         else
  26.             c:Print("[bmd.scriptapp] Error: Please open the Fusion GUI before running this tool.\n\n")
  27.         end
  28.     else                            -- The script was run from inside Fusion
  29.         fu = fusion
  30.         c = composition
  31.         c:Print("Local control.\n")
  32.     end
  33.  
  34. end
  35.  
  36. setEnvirons()
  37.  
  38. -- Create a new window
  39. local ui = fu.UIManager
  40. local disp = bmd.UIDispatcher(ui)
  41. local width,height = 400,200
  42.  
  43. win = disp:AddWindow({
  44.   ID = 'MyWin',
  45.   WindowTitle = 'My First Window',
  46.   Geometry = {100, 100, width, height},
  47.   Spacing = 10,
  48.  
  49.   ui:VGroup{
  50.     ID = 'root',
  51.    
  52.     -- Add your GUI elements here:
  53.     ui:Label{
  54.       ID = 'TextLabel',
  55.       Text = 'This is a Label',
  56.     },
  57.   },
  58. })
  59.  
  60. -- The window was closed
  61. function win.On.MyWin.Close(ev)
  62.   disp:ExitLoop()
  63. end
  64.  
  65. -- Add your GUI element based event functions here:
  66. itm = win:GetItems()
  67.  
  68. win:Show()
  69. disp:RunLoop()
  70. win:Hide()

I've banged together a visual reference for how I imagine the window might be laid out. Doing this prompts more thinking about how the script could work, what it might be like to interact with it, and sometimes it points out problems I hadn't considered.

012-pipeline_107_UI-mockup.png

While I was mocking up the interface, I thought it would be cool to be able to twirl down a Loader set to see all the Loaders it contains, and it would be nice to be able to lock a buffer in case we don't want it to update. The Version column will have combo boxes from which the user can simply select the version they want to change to. A UI Manager combo box can execute code upon being changed, so we don't even need a commit button of any kind—the Loaders will change immediately upon selection. I've put a feature in where the number of Loaders in the set is displayed. This will mean that we need to keep track of how many Loaders match a particular pattern, and since we're tracking multiple patterns, maybe we should store the Loaders in subtables of the pattern table instead of giving them their own table.

Do we even need to keep track of how many Loaders the user selected? With this interface staring at me, I realize that we could just put all of the Loaders in it every time.

A problem occurs to me in that if a Loader's been locked (I think I'll change that 'Lock' to a lock icon), the script as currently written won't find that Loader—the pattern matching goes all the way to the version number. The next time the script is run, that Loader won't show up in the list. The easiest way to fix that would be to simply truncate the version number folder from the pattern. I'm not going to do that quite yet, though.

In short, maybe I should have done this mockup before I even started coding! But that's the way scripting goes sometimes—don't be afraid of throwing away work you've done if you discover a better way. I'm not quite to the point of trashing my work yet, but I have a feeling that it may be coming. For now, onward!

Well… Maybe not quite yet with the onward. I'm going to take a break here because although I know that a twirl-down is available in the Tree widget in UI Manager, I don't know how it works yet. Andrew kindly pointed me toward Reactor as an example of what I want to do, so I'm going to take a couple of days to study and experiment before I move on.
You do not have the required permissions to view the files attached to this post.

User avatar
Cedric
Fusioneer
Posts: 54
Joined: Tue Sep 13, 2016 7:26 am
Answers: 1
Location: Ghent
Been thanked: 6 times

Re: Building a Version Control script — collaborative learning

#25

Post by Cedric » Thu Feb 15, 2018 12:58 pm

AndrewHazelden wrote:
Thu Feb 08, 2018 7:19 pm
Midgardsormr wrote:
Thu Feb 08, 2018 1:36 pm
In order to test my logic, I put all of this new code into Sublime and attempted to execute it remotely. I ran into a problem: when executed from outside, FuScript apparently doesn't know about the bmd.scriptlib. bmd.split() doesn't work! Okay, I can think of a couple of ways to get around this:
  • I can use an environment variable in the operating system to provide a path to the scriptlib. This would have the advantage of giving me a place to put other Lua libraries, and I could access any of them from my Fusion scripts. But I'd have to build in a mechanism to assist the user in setting up the variable and the library repository. That's probably a no-go for most users.
  • ...
  • I can try to query the pathmap to the Scripts directory and reverse it to get the location of bmd.scriptlib. The problems with that are a) I have no idea how to do it, although I'm sure I could find a code sample that would teach me, and b) it seems overly complex in comparison to option 3.
You could try using Lua code like this to locate and source the scriptlib file so its functions are accessible inside of a FuScript terminal session:

Code: Select all

ldofile(fu:MapPath("Scripts:/bmd.scriptlib"))
My guess is your scriptlib sourcing issues are possibly related to having Fu 9.0.1 still being installed on one of your systems as some changes were made in v9.0.2 to make the scriptlib loading work more reliably the before from the Fusion Console tab/Script menu. :)

If you are going to clone some of the functions from the bmd.scriptlib file and update them, here is a revised pathIsMovieFormat() function I used inside of the Archive Composition script that includes entries for the new movie formats added in Fusion 9.

Code: Select all

function pathIsMovieFormat(path)
	local extension = bmd.getextension(path):lower()
	if extension ~= nil then
		if ( extension == "3gp" ) or
				( extension == "aac" ) or
				( extension == "aif" ) or
				( extension == "aiff" ) or
				( extension == "avi" ) or
				( extension == "dvs" ) or
				( extension == "fb"  ) or
				( extension == "flv" ) or
				( extension == "m2ts" ) or
				( extension == "m4a" ) or
				( extension == "m4b" ) or
				( extension == "m4p" ) or
				( extension == "mkv" ) or
				( extension == "mov" ) or
				( extension == "mp3" ) or
				( extension == "mp4" ) or
				( extension == "mts" ) or
				( extension == "mxf" ) or
				( extension == "omf" ) or
				( extension == "omfi" ) or
				( extension == "qt" ) or
				( extension == "stm" ) or
				( extension == "tar" ) or
				( extension == "vdr" ) or
				( extension == "vpv" ) or
				( extension == "wav" ) or
				( extension == "webm" ) then
			return true
		end
	end
	return false
end
I also added a new audio file checking function to the Archive Composition script too and that code might be of some use if you ever end up needing to check the file extensions of all the media types loaded in a Loader/Saver node (SoundFilename)/Fusion timeline (COMPS_AudioFilename)/SuckLessAudio Modifier fuse (Fuse.SuckLessAudio - WaveFile[0]):

Code: Select all

function pathIsAudioFormat(path)
	local extension = bmd.getextension(path):lower()
	if extension ~= nil then
		if  ( extension == "aac" ) or
				( extension == "aif" ) or
				( extension == "aiff" ) or
				( extension == "m4a" ) or
				( extension == "mp3" ) or
				( extension == "wav" ) then
			return true
		end
	end
	return false
end
Hi Andrew

I was browsing through some threads and ended up discussing code stuff with Kristof about what you wrote here.
He told me to post it because an artist might not directly think about making his code more dynamic without refactoring prematurely and such.
Good practice for all of us. So if you don't mind, I refactored it and here is the result.

Cheers
Cedric
Code: [Select all] [Expand/Collapse] [Download] (refactored.lua)
  1. -- ===========================================================================
  2. -- constants
  3. -- ===========================================================================
  4. -- valid movie extensions
  5. VALID_MOVIE_EXTENSIONS = {
  6.     aac=true,
  7.     aif=true,
  8.     aif=true,
  9.     avi=true,
  10.     dvs=true,
  11.     fb=true,
  12.     flv=true,
  13.     m2t=true,
  14.     m4a=true,
  15.     m4b=true,
  16.     m4p=true,
  17.     mkv=true,
  18.     mov=true,
  19.     mp3=true,
  20.     mp4=true,
  21.     mts=true,
  22.     mxf=true,
  23.     omf=true,
  24.     omfi=true,
  25.     qt=true,
  26.     stm=true,
  27.     tar=true,
  28.     vdr=true,
  29.     vpv=true,
  30.     wav=true,
  31.     web=true
  32. }
  33. -- keys starting with digits
  34. VALID_MOVIE_EXTENSIONS["3gp"] = true
  35.  
  36. VALID_AUDIO_EXTENSIONS = {
  37.     aac=true,
  38.     aif=true,
  39.     aiff=true,
  40.     m4a=true,
  41.     mp3=true,
  42.     wav=true
  43. }
  44.  
  45. -- ===========================================================================
  46. -- functions
  47. -- ===========================================================================
  48. function validateExtension(extension, valid_values)
  49.     -- checks if the given extension is in the given valid values table
  50.     if extension and valid_values[extension] then
  51.         return true
  52.     end
  53.     return false
  54. end
  55.  
  56. function isMovieExtension(extension)
  57.     -- checks if the given extension is a valid movie extension
  58.     return validateExtension(extension, VALID_MOVIE_EXTENSIONS)
  59. end
  60.  
  61. function isAudioExtension(extension)
  62.     -- checks if the given extension is a valid audio extension
  63.     return validateExtension(extension, VALID_AUDIO_EXTENSIONS)
  64. end
  65.  
  66. function pathIsMovieFormat(path)
  67.     -- checks if the given path has a valid movie extension
  68.     local extension = bmd.getextension(path):lower()
  69.     return isMovieExtension(extension)
  70.     -- return validateExtension(extension, VALID_MOVIE_EXTENSIONS)  -- use this if you don't use isMovieExtension anywhere else
  71. end
  72.  
  73. function pathIsAudioFormat(path)
  74.     -- checks if the given path has a valid audio extension
  75.     local extension = bmd.getextension(path):lower()
  76.     return isAudioExtension(extension)
  77.     -- return validateExtension(extension, VALID_AUDIO_EXTENSIONS)  -- use this if you don't use isAudioExtension anywhere else
  78. end
  79.  
  80. -- test if the function works
  81. print(isMovieExtension(nil))  -- false
  82. print(isMovieExtension("mp3"))  -- false
  83. print(isMovieExtension("web"))  -- true
  84.  
  85. print(isAudioExtension(nil))  -- false
  86. print(isAudioExtension("3gp"))  -- false
  87. print(isAudioExtension("mp3"))  -- true

User avatar
AndrewHazelden
Fusionator
Posts: 1648
Joined: Fri Apr 03, 2015 3:20 pm
Answers: 9
Location: West Dover, Nova Scotia, Canada
Been thanked: 21 times
Contact:

Re: Building a Version Control script — collaborative learning

#26

Post by AndrewHazelden » Thu Feb 15, 2018 1:49 pm

Cedric wrote:
Thu Feb 15, 2018 12:58 pm
I was browsing through some threads and ended up discussing code stuff with Kristof about what you wrote here.
He told me to post it because an artist might not directly think about making his code more dynamic without refactoring prematurely and such.
Good practice for all of us. So if you don't mind, I refactored it and here is the result.
Hi Cedric.

Thanks for sharing that! :)

I do think I should mention that I have spent a lot of time after Reactor was released debating if I should really be using the Fusion bmd.scriptlib file at all for that sort of task in my own newer scripted workflows for new Lua scripts made fresh in the year 2018 and beyond. I only posted the code here since was handy for people using external FuScript processes like Notepad++, Sublime, BBedit which is relevant to the context of the specific version control script thread. :)

If I wanted to make things fully Fusion 9 modernized and less hard-coded feeling I would recommend people explore probing the Fusion registry system where you can read all of the supported image formats, movie formats, 3D model formats, and audio formats that are unlocked in Fusion at this second by Fusion's core features or any of the extra loaded plugins you install, along with formats provided to Fusion by the addon FFMPEG:/ PathMap library and FFmpeg shared library installation approach on your system.

These thoughts about whether it is best to use the semi-legacy bmd.scriptlib file with its known issues vs reading the native Fusion registry and knowing *exactly* what is possible for the current active host Fusion system came about after I spent the time to make the "Fuse Scanner", and "Plugin Scanner" Lua scripts that are in Reactor's Scripts > Reactor category.

The reason I am mentioning this now is that I have been looking at the idea of making a sequel to those "Scanner" tools of a possible "Macros Scanner" script that would help find duplicate items installed in the Macros: PathMap folders and also list what fuses/plugins are missing if you wanted to use that exact macro on your system.

Then finally if life slows down enough and I can somehow magically find enough spare time, a "Registry Scanner" script would be awesome that could in a visual way list each .dll library, or fuse, or OFX plugin that is active and loaded in Fusion's registry. I'm thinking that would be best done with a UI manager based GUI that has a main ui:Tree view with an expandable/collapsable foldouts added for each library. When you expand it outwards that library entry line show each Fusion feature it provides to the user with. This would be super handy for probing under documented API features that are sitting there on everyone copy of Fuison (Free) and Fusion Studio.

Related to your refactored code @Cedric, only the Fusion registry has all of the real data that reflects what the current Fusion (Free)/ Fusion Studio version on Windows, MacOS, and Linux can use as far as movie and audio extension go. The Scriptlib approach is kind of a bad workflow now as it never updates over time as new movie and audio formats are continuously added and removed. The fact BMD didn't update the bmd.scriptlib file at Fusion 9's release to reflect what media formats are enabled further reflects why using the scriptlib is not a good idea in the long term (IMHO).

User avatar
Midgardsormr
Fusionator
Posts: 1823
Joined: Wed Nov 26, 2014 8:04 pm
Answers: 15
Location: Los Angeles, CA, USA
Been thanked: 112 times
Contact:

Re: Building a Version Control script — collaborative learning

#27

Post by Midgardsormr » Thu Feb 15, 2018 4:37 pm

Thanks for that advice, Cedric! I know I have lots of room for improvement, although I'm not yet skilled enough to be able to identify my own bad practices.

I spent a few hours tinkering with code I stole from Reactor and the UI Manager examples, and I think I've got a handle on how to deal with at least part of my interface. I had hoped that Reactor's use of checkboxes in a Tree meant that I might be able to embed other kinds of UI features into it, too. Unfortunately, I didn't see any way of putting a combo box in there. For now, I think I'll have to content myself with popping up a second window for version selection. Other than that, though, the UI looks much as I imagined it would:

012-pipeline_108_UI-prototype.png

I'm not sure if it's possible to assign a different font to just one column of the Tree—I wasn't able to do it when I tried, but I admit that I didn't try all that hard—so I'm sticking with the word "Lock" instead of an icon for the time being. Symbola looks just weird as the font for the entire Tree. The following code is my testing prototype. It has just enough dynamic properties to prove to myself that everything was going to work.

Code: [Select all] [Expand/Collapse] [Download] (windowTest.lua)
  1. ----------------------------------------------------
  2. -- setEnvirons()
  3. --
  4. -- Creates a connection to Fusion if the script is
  5. -- run from an external process. Creates shortcut
  6. -- variables c and fu for composition and fusion,
  7. -- respectively.
  8. ----------------------------------------------------
  9. function setEnvirons()
  10.     if fusion == nil then           -- We're operating remotely.
  11.         -- Connect to Fusion on the local system
  12.         hostname = "localhost"
  13.         fusion = bmd.scriptapp("Fusion", hostname)
  14.  
  15.         -- Set a quick shortcut to the Fusion object
  16.         fu = fusion
  17.  
  18.         -- Test to be sure Fusion is running and print a confirmation to the console.
  19.         if fusion ~= nil then
  20.             c = fusion.CurrentComp  -- Set a shortcut to the composition object.
  21.             SetActiveComp(c)
  22.            
  23.             ldofile(fu:MapPath("Scripts:/bmd.scriptlib"))       -- Load the bmd library
  24.             c:Print("[bmd.scriptapp] Connected to Fusion on \"" .. hostname .. "\"\n\n")
  25.         else
  26.             print("[bmd.scriptapp] Error: Please open the Fusion GUI before running this tool.\n\n")
  27.         end
  28.     else                            -- The script was run from inside Fusion
  29.         fu = fusion
  30.         c = composition
  31.     end
  32.  
  33. end
  34.  
  35. -- Creates the Version Control window
  36. function MainWindow()
  37.     local width,height = 1200,100
  38.     win = disp:AddWindow({
  39.         ID = 'VersionControl',
  40.         WindowTitle = 'Loaders Version Control',
  41.         Geometry = {100, 100, width, height},
  42.  
  43.      
  44.         ui:VGroup{
  45.             ID = 'root',
  46.             ui:Tree{
  47.                 ID = 'Tree',
  48.                 SortingEnabled = true,
  49.                 Events = {
  50.                     ItemClicked = true,
  51.                 },
  52.             },
  53.         },
  54.     })
  55.  
  56.     -- Add your GUI element based event functions here:
  57.     itm = win:GetItems()
  58.  
  59.     -- Configure the Tree
  60.     itm.Tree.ColumnCount = 4
  61.  
  62.     itm.Tree.ColumnWidth[0] = 180
  63.     itm.Tree.ColumnWidth[1] = 60
  64.     itm.Tree.ColumnWidth[2] = 40
  65.     itm.Tree.ColumnWidth[3] = 300
  66.  
  67.     -- Add a header row
  68.     hdr = itm.Tree:NewItem()
  69.     hdr.Text[0] = "Loader Set"
  70.     hdr.Text[1] = 'Version'
  71.     hdr.Text[2] = 'Lock'
  72.     hdr.Text[3] = 'File Path'
  73.     itm.Tree:SetHeaderItem(hdr)
  74.  
  75.     -- Add Tree Rows (Replace with a call to PopulateLoaderTree)
  76.  
  77.     itRow = itm.Tree:NewItem()
  78.     itRow.Text[0] = 'mattePainting         (3)'
  79.     itRow.Text[1] = 'v03'
  80.     itRow.Text[2] = ''
  81.     itRow.Text[3] = 'D:/projects/foo/elements/mattePainting/v03/'
  82.     itm.Tree:AddTopLevelItem(itRow)
  83.  
  84.     itChild = itm.Tree:NewItem()
  85.     itChild:SetTextColor(1,0,0)
  86.     itChild.Text[0] = 'FG'
  87.     itChild.Text[1] = 'v03'
  88.     itChild.Text[2] = ''
  89.     itChild.Text[3] = 'D:/projects/foo/elements/mattePainting/v03/FG/201_030_mattePainting-FG_v03.exr'
  90.     itChild.CheckState[2] = "Unchecked"
  91.     itRow:AddChild(itChild)
  92.  
  93.  
  94.     -- The window was closed
  95.     function win.On.VersionControl.Close(ev)
  96.         disp:ExitLoop()
  97.     end
  98.  
  99.     -- A tree row was clicked on
  100.     function win.On.Tree.ItemClicked(ev)
  101.         if ev.column == 1 then
  102.             ChooseVersion()
  103.         end
  104.     end
  105.  
  106. end
  107.  
  108.  
  109. -- Display the Choose Version Window
  110. function ChooseVersion()
  111.     local width,height = 250,45
  112.  
  113.     winVer = disp:AddWindow({
  114.         ID = 'versionWin',
  115.         WindowTitle = 'Choose New Version',
  116.         WindowFlags = {Window = true, WindowStaysOnTopHint = true,},
  117.         Geometry = {130, 200, width, height},
  118.  
  119.  
  120.         ui:VGroup{
  121.             ID = 'root',
  122.             ui:ComboBox{
  123.                 ID = 'VersionList',
  124.                 Text = 'version',
  125.             },
  126.         },
  127.     })
  128.  
  129.     -- Add your GUI element based event functions here:
  130.     itmVer = winVer:GetItems()
  131.  
  132.     versionList = populateVersionList()
  133.  
  134.     for i, j in ipairs(versionList) do
  135.         itmVer.VersionList:AddItem(j)
  136.     end
  137.  
  138.     function winVer.On.versionWin.Close(ev)
  139.         winVer:Hide()
  140.     end
  141.  
  142.     function winVer.On.VersionList.CurrentIndexChanged(ev)
  143.         index = itmVer.VersionList.CurrentIndex + 1
  144.         NewVersion = versionList[index]
  145.         itRow.Text[1] = NewVersion
  146.     end
  147.  
  148.     winVer:Show()
  149.  
  150. end
  151.  
  152. function populateVersionList()
  153.     local list = {}
  154.     list[1] = 'v01'
  155.     list[2] = 'v02'
  156.     list[3] = 'v03'
  157.     list[4] = 'v04'
  158.     return list
  159. end
  160.  
  161. setEnvirons()
  162.  
  163.  
  164.  
  165. -- Set up aliases to the UI Manager framework
  166. ui = fu.UIManager
  167. disp = bmd.UIDispatcher(ui)
  168.  
  169.  
  170. -- Create a global
  171. NewVersion = ''
  172.  
  173. MainWindow()
  174.  
  175.  
  176.  
  177. win:Show()
  178. disp:RunLoop()
  179. win:Hide()
  180. winVer:Hide()

So let's walk through some of that to see what's happening. The setEnvirons() function is, of course, creating the connection between Sublime and Fusion, as before. Then we have a function called MainWindow(). This function calls up the main list of Loaders and will be the primary point of interaction for the user. I'll dig into the functions one by one in a bit; for now, let's just get an overview. Next up is ChooseVersion(), which creates the pop-up window from which the user can choose a new version. This window appears when the user clicks on an existing version number in the main window. populateVersionList() creates a list of available versions. Eventually this routine will scan the actual available version folders, but for now it just returns a hard-coded list for testing purposes.

After the function definitions, we proceed with lines that actually run the program. Lines 166 and 167 give us some handles to the UI Manager framework; ui links us to all of the available tools available with UI Manager, and disp is used to control the display of the windows and their interactivity. These are global variables, so they are visible inside the functions. The final three lines control the visibility of the windows. win:Show() makes the MainWindow visible. disp:RunLoop() allows the window to maintain interactivity—it continually monitors the state of the UI's components to detect changes. And once the loop has been broken, the final line hides the window.

  1.     local width,height = 1200,100
  2.     win = disp:AddWindow({
  3.         ID = 'VersionControl',
  4.         WindowTitle = 'Loaders Version Control',
  5.         Geometry = {2020, 100, width, height},
  6.  
  7.      
  8.         ui:VGroup{
  9.             ID = 'root',
  10.             ui:Tree{
  11.                 ID = 'Tree',
  12.                 SortingEnabled = true,
  13.                 Events = {
  14.                     ItemClicked = true,
  15.                 },
  16.             },
  17.         },
  18.     })

Back to the top we go to take a look at MainWindow(). Creating the layout for the GUI is done with the AddWindow() method. It gets just one argument, but that argument usually consists of a table with several sub-tables. UI Manager's Event handling requires the window to have an ID, so that's a required key in the table. The WindowTitle determines the name at the top of the window. Geometry determines the size and location of the window. I know there's a way to get a window to appear near the mouse, and I'll probably use it later on, but for now, the UI will always appear at the same location. Width and height will probably eventually be dynamic, dependent on the number and length of paths we're dealing with, so they've gone in as variables.

Now we come to the actual widgets that go into the window. In this case, we have only two elements: A VGroup, which serves as a container for everything else and has the ID root, and a Tree. A Tree is similar in appearance to a spreadsheet. We want to create an Event when an item is clicked, so we give the Tree an attribute Events and set ItemClicked = true. There is quite a bit more information about the different sorts of widgets that can go in a UI Manager window in the main UI Manager thread, and many examples can be found in the sample scripts available in Reactor.

  1.     -- Add your GUI element based event functions here:
  2.     itm = win:GetItems()
  3.  
  4.     -- Configure the Tree
  5.     itm.Tree.ColumnCount = 4
  6.  
  7.     itm.Tree.ColumnWidth[0] = 180
  8.     itm.Tree.ColumnWidth[1] = 60
  9.     itm.Tree.ColumnWidth[2] = 40
  10.     itm.Tree.ColumnWidth[3] = 300
  11.  
  12.     -- Add a header row
  13.     hdr = itm.Tree:NewItem()
  14.     hdr.Text[0] = "Loader Set"
  15.     hdr.Text[1] = 'Version'
  16.     hdr.Text[2] = 'Lock'
  17.     hdr.Text[3] = 'File Path'
  18.     itm.Tree:SetHeaderItem(hdr)
  19.  
  20.     -- Add Tree Rows (Replace with a call to PopulateLoaderTree)
  21.  
  22.     itRow = itm.Tree:NewItem()
  23.     itRow.Text[0] = 'mattePainting         (3)'
  24.     itRow.Text[1] = 'v03'
  25.     itRow.Text[2] = ''
  26.     itRow.Text[3] = 'D:/projects/foo/elements/mattePainting/v03/'
  27.     itm.Tree:AddTopLevelItem(itRow)
  28.  
  29.     itChild = itm.Tree:NewItem()
  30.     itChild.Text[0] = 'FG'
  31.     itChild.Text[1] = 'v03'
  32.     itChild.Text[2] = ''
  33.     itChild.Text[3] = 'D:/projects/foo/elements/mattePainting/v03/FG/201_030_mattePainting-FG_v03.exr'
  34.     itChild.CheckState[2] = "Unchecked"
  35.     itRow:AddChild(itChild)

With the window defined and its layout set, we query it for a list of its items with GetItems(). This fills the table itm with each widget's ID as a key. We can then access the various traits of the Tree using the address itm.Tree. Setting the header and adding rows works just as Andrew described in his thread, so I won't bother repeating it here. The bits that Andrew has yet to demonstrate are the use of the disclosure triangle to create nested rows and inserting a checkbox into one of the fields.

Most of the rows in a Tree are created with the method AddTopLevelItem(). In order to add a child under such a top level row, we need to keep track of the handle to the parent row, so we can't just use a loop like in Andrew's Tree demo—each row that goes in overwrites the handle, so we could only add child items to the very last row. That means that we'll need to either add the buffers before moving on to the next Loader Set or we'll need to keep a table of the Loader Set rows so that we can add children to them later. Either way will work, but we haven't gotten to that point quite yet. What I have here shows that itRow holds the results from NewItem(), and I use the AddChild() method on itRow to create the sub-item. I've also made itRow a global variable so that it can be modified from the other function.

The checkbox is very easy to add. Each column has a CheckState field, but if it doesn't contain either 'Checked' or 'Unchecked', the checkbox will not be displayed. Setting itChild.CheckState[2] = 'beef' has the same effect as not including the line at all—no checkbox.

I should mention also the SetTextColor() line. I haven't yet worked out how to use it, so that line doesn't actually do anything (as far as I can tell).

  1.     -- The window was closed
  2.     function win.On.VersionControl.Close(ev)
  3.         disp:ExitLoop()
  4.     end
  5.  
  6.     -- A tree row was clicked on
  7.     function win.On.Tree.ItemClicked(ev)
  8.         if ev.column == 1 then
  9.             ChooseVersion()
  10.         end
  11.     end

Next we have the event handling functions. ev is a special variable that contains information about what interactions the user has made. When the user clicks on a field in the Tree, the event function win.On.Tree.ItemClicked() activates. Since I want to only pop up the ChooseVersion window when the version field itself is clicked (not, for instance, when the Lock checkbox is changed), I have used an if statement that tests which for column 1. The function will be called no matter which field the user clicks, but it will do nothing if any field other than the version number was clicked.

If the user did click a version number field, then the ChooseVersion() function executes. Let's take a look at that one.

  1. function ChooseVersion()
  2.     local width,height = 250,45
  3.  
  4.     winVer = disp:AddWindow({
  5.         ID = 'versionWin',
  6.         WindowTitle = 'Choose New Version',
  7.         Geometry = {2050, 200, width, height},
  8.  
  9.         ui:VGroup{
  10.             ID = 'root',
  11.             ui:ComboBox{
  12.                 ID = 'VersionList',
  13.                 Text = 'version',
  14.             },
  15.         },
  16.     })
  17.  
  18.     -- Add your GUI element based event functions here:
  19.     itmVer = winVer:GetItems()
  20.  
  21.     versionList = populateVersionList()
  22.  
  23.     for i, j in ipairs(versionList) do
  24.         itmVer.VersionList:AddItem(j)
  25.     end
  26.  
  27.     function winVer.On.versionWin.Close(ev)
  28.         winVer:Hide()
  29.     end
  30.  
  31.     function winVer.On.VersionList.CurrentIndexChanged(ev)
  32.         index = itmVer.VersionList.CurrentIndex + 1
  33.         NewVersion = versionList[index]
  34.         itRow.Text[1] = NewVersion
  35.     end
  36.  
  37.     winVer:Show()
  38.  
  39. end

It is very similar to the previous function; this one uses a ComboBox widget instead of the Tree. Notice that I am using a different variable name to create the window and collect the item list. Since these variables are global in scope, I want to be sure that I don't accidentally overwrite them—I'll need to be able to access the MainWindow's item list later on, so those handles need to be preserved. As I mentioned earlier, I'm using a separate function to populate the list of versions, something that I'll need to do in MainWindow() as well if I want to keep the script well organized and easy to understand. When the ComboBox is changed, winVer.On.VersionList.CurrentIndexChanged() executes. Unlike most Lua tables, the ComboBox's indices start at 0 instead of 1, so I immediately add 1 to the index so that I don't get confused. I did a few iterations through getting the new version number when I was trying to understand what was going on, so the function has a line it really doesn't need. I could have just written it.Row.Text[1] = versionList[index] instead of passing it through another variable. At the end of the function, I call the window's Show() method to cause it to display. And let's rewind a bit to the Close command, where instead of exiting the loop, as is usually done, I simply hide this window. If I used disp:ExitLoop(), then the entire script would end instead of just closing the one window.

There's no reason to discuss populateVersionList() because it's just a placeholder for a more complicated procedure, so that concludes this look at the UI Manager prototype! Next time I'll import the code into the main script and start working out the functions I'll need to populate the lists.
You do not have the required permissions to view the files attached to this post.
Last edited by Midgardsormr on Fri Feb 16, 2018 11:48 am, edited 1 time in total.

User avatar
Cedric
Fusioneer
Posts: 54
Joined: Tue Sep 13, 2016 7:26 am
Answers: 1
Location: Ghent
Been thanked: 6 times

Re: Building a Version Control script — collaborative learning

#28

Post by Cedric » Thu Feb 15, 2018 11:36 pm

Hi Andrew
Hi Cedric.

Thanks for sharing that! :)
No problem!
I do think I should mention that I have spent a lot of time after Reactor was released debating if I should really be using the Fusion bmd.scriptlib file at all for that sort of task in my own newer scripted workflows for new Lua scripts made fresh in the year 2018 and beyond. I only posted the code here since was handy for people using external FuScript processes like Notepad++, Sublime, BBedit which is relevant to the context of the specific version control script thread. :)
I'd opt for not using the scriptlib for these kind of standalone functions, just because the real functionality has no requirement towards it,
If I wanted to make things fully Fusion 9 modernized and less hard-coded feeling I would recommend people explore probing the Fusion registry system where you can read all of the supported image formats, movie formats, 3D model formats, and audio formats that are unlocked in Fusion at this second by Fusion's core features or any of the extra loaded plugins you install, along with formats provided to Fusion by the addon FFMPEG:/ PathMap library and FFmpeg shared library installation approach on your system.
...
Related to your refactored code @Cedric, only the Fusion registry has all of the real data that reflects what the current Fusion (Free)/ Fusion Studio version on Windows, MacOS, and Linux can use as far as movie and audio extension go. The Scriptlib approach is kind of a bad workflow now as it never updates over time as new movie and audio formats are continuously added and removed. The fact BMD didn't update the bmd.scriptlib file at Fusion 9's release to reflect what media formats are enabled further reflects why using the scriptlib is not a good idea in the long term (IMHO).
I actually never had the need to get those kinds of "default" valid Fusion values such as supported exentension (apart from channels) so now that you mention, indeed, parsing the registry for those valid values is good idea. No need to keep the constants (which could be a settings file for real easy access) up to date with your needs. Do you know the syntax to get these values by the way?

Cheers
Cedric

User avatar
Cedric
Fusioneer
Posts: 54
Joined: Tue Sep 13, 2016 7:26 am
Answers: 1
Location: Ghent
Been thanked: 6 times

Re: Building a Version Control script — collaborative learning

#29

Post by Cedric » Fri Feb 16, 2018 3:51 pm

Midgardsormr wrote:
Thu Feb 15, 2018 4:37 pm
Thanks for that advice, Cedric! I know I have lots of room for improvement, although I'm not yet skilled enough to be able to identify my own bad practices.

I spent a few hours tinkering with code I stole from Reactor and the UI Manager examples, and I think I've got a handle on how to deal with at least part of my interface. I had hoped that Reactor's use of checkboxes in a Tree meant that I might be able to embed other kinds of UI features into it, too. Unfortunately, I didn't see any way of putting a combo box in there. For now, I think I'll have to content myself with popping up a second window for version selection. Other than that, though, the UI looks much as I imagined it would:


012-pipeline_108_UI-prototype.png


I'm not sure if it's possible to assign a different font to just one column of the Tree—I wasn't able to do it when I tried, but I admit that I didn't try all that hard—so I'm sticking with the word "Lock" instead of an icon for the time being. Symbola looks just weird as the font for the entire Tree. The following code is my testing prototype. It has just enough dynamic properties to prove to myself that everything was going to work.

Code: [Select all] [Expand/Collapse] [Download] (windowTest.lua)
  1. ----------------------------------------------------
  2. -- setEnvirons()
  3. --
  4. -- Creates a connection to Fusion if the script is
  5. -- run from an external process. Creates shortcut
  6. -- variables c and fu for composition and fusion,
  7. -- respectively.
  8. ----------------------------------------------------
  9. function setEnvirons()
  10.     if fusion == nil then           -- We're operating remotely.
  11.         -- Connect to Fusion on the local system
  12.         hostname = "localhost"
  13.         fusion = bmd.scriptapp("Fusion", hostname)
  14.  
  15.         -- Set a quick shortcut to the Fusion object
  16.         fu = fusion
  17.  
  18.         -- Test to be sure Fusion is running and print a confirmation to the console.
  19.         if fusion ~= nil then
  20.             c = fusion.CurrentComp  -- Set a shortcut to the composition object.
  21.             SetActiveComp(c)
  22.            
  23.             ldofile(fu:MapPath("Scripts:/bmd.scriptlib"))       -- Load the bmd library
  24.             c:Print("[bmd.scriptapp] Connected to Fusion on \"" .. hostname .. "\"\n\n")
  25.         else
  26.             print("[bmd.scriptapp] Error: Please open the Fusion GUI before running this tool.\n\n")
  27.         end
  28.     else                            -- The script was run from inside Fusion
  29.         fu = fusion
  30.         c = composition
  31.     end
  32.  
  33. end
  34.  
  35. -- Creates the Version Control window
  36. function MainWindow()
  37.     local width,height = 1200,100
  38.     win = disp:AddWindow({
  39.         ID = 'VersionControl',
  40.         WindowTitle = 'Loaders Version Control',
  41.         Geometry = {100, 100, width, height},
  42.  
  43.      
  44.         ui:VGroup{
  45.             ID = 'root',
  46.             ui:Tree{
  47.                 ID = 'Tree',
  48.                 SortingEnabled = true,
  49.                 Events = {
  50.                     ItemClicked = true,
  51.                 },
  52.             },
  53.         },
  54.     })
  55.  
  56.     -- Add your GUI element based event functions here:
  57.     itm = win:GetItems()
  58.  
  59.     -- Configure the Tree
  60.     itm.Tree.ColumnCount = 4
  61.  
  62.     itm.Tree.ColumnWidth[0] = 180
  63.     itm.Tree.ColumnWidth[1] = 60
  64.     itm.Tree.ColumnWidth[2] = 40
  65.     itm.Tree.ColumnWidth[3] = 300
  66.  
  67.     -- Add a header row
  68.     hdr = itm.Tree:NewItem()
  69.     hdr.Text[0] = "Loader Set"
  70.     hdr.Text[1] = 'Version'
  71.     hdr.Text[2] = 'Lock'
  72.     hdr.Text[3] = 'File Path'
  73.     itm.Tree:SetHeaderItem(hdr)
  74.  
  75.     -- Add Tree Rows (Replace with a call to PopulateLoaderTree)
  76.  
  77.     itRow = itm.Tree:NewItem()
  78.     itRow.Text[0] = 'mattePainting         (3)'
  79.     itRow.Text[1] = 'v03'
  80.     itRow.Text[2] = ''
  81.     itRow.Text[3] = 'D:/projects/foo/elements/mattePainting/v03/'
  82.     itm.Tree:AddTopLevelItem(itRow)
  83.  
  84.     itChild = itm.Tree:NewItem()
  85.     itChild:SetTextColor(1,0,0)
  86.     itChild.Text[0] = 'FG'
  87.     itChild.Text[1] = 'v03'
  88.     itChild.Text[2] = ''
  89.     itChild.Text[3] = 'D:/projects/foo/elements/mattePainting/v03/FG/201_030_mattePainting-FG_v03.exr'
  90.     itChild.CheckState[2] = "Unchecked"
  91.     itRow:AddChild(itChild)
  92.  
  93.  
  94.     -- The window was closed
  95.     function win.On.VersionControl.Close(ev)
  96.         disp:ExitLoop()
  97.     end
  98.  
  99.     -- A tree row was clicked on
  100.     function win.On.Tree.ItemClicked(ev)
  101.         if ev.column == 1 then
  102.             ChooseVersion()
  103.         end
  104.     end
  105.  
  106. end
  107.  
  108.  
  109. -- Display the Choose Version Window
  110. function ChooseVersion()
  111.     local width,height = 250,45
  112.  
  113.     winVer = disp:AddWindow({
  114.         ID = 'versionWin',
  115.         WindowTitle = 'Choose New Version',
  116.         WindowFlags = {Window = true, WindowStaysOnTopHint = true,},
  117.         Geometry = {130, 200, width, height},
  118.  
  119.  
  120.         ui:VGroup{
  121.             ID = 'root',
  122.             ui:ComboBox{
  123.                 ID = 'VersionList',
  124.                 Text = 'version',
  125.             },
  126.         },
  127.     })
  128.  
  129.     -- Add your GUI element based event functions here:
  130.     itmVer = winVer:GetItems()
  131.  
  132.     versionList = populateVersionList()
  133.  
  134.     for i, j in ipairs(versionList) do
  135.         itmVer.VersionList:AddItem(j)
  136.     end
  137.  
  138.     function winVer.On.versionWin.Close(ev)
  139.         winVer:Hide()
  140.     end
  141.  
  142.     function winVer.On.VersionList.CurrentIndexChanged(ev)
  143.         index = itmVer.VersionList.CurrentIndex + 1
  144.         NewVersion = versionList[index]
  145.         itRow.Text[1] = NewVersion
  146.     end
  147.  
  148.     winVer:Show()
  149.  
  150. end
  151.  
  152. function populateVersionList()
  153.     local list = {}
  154.     list[1] = 'v01'
  155.     list[2] = 'v02'
  156.     list[3] = 'v03'
  157.     list[4] = 'v04'
  158.     return list
  159. end
  160.  
  161. setEnvirons()
  162.  
  163.  
  164.  
  165. -- Set up aliases to the UI Manager framework
  166. ui = fu.UIManager
  167. disp = bmd.UIDispatcher(ui)
  168.  
  169.  
  170. -- Create a global
  171. NewVersion = ''
  172.  
  173. MainWindow()
  174.  
  175.  
  176.  
  177. win:Show()
  178. disp:RunLoop()
  179. win:Hide()
  180. winVer:Hide()

So let's walk through some of that to see what's happening. The setEnvirons() function is, of course, creating the connection between Sublime and Fusion, as before. Then we have a function called MainWindow(). This function calls up the main list of Loaders and will be the primary point of interaction for the user. I'll dig into the functions one by one in a bit; for now, let's just get an overview. Next up is ChooseVersion(), which creates the pop-up window from which the user can choose a new version. This window appears when the user clicks on an existing version number in the main window. populateVersionList() creates a list of available versions. Eventually this routine will scan the actual available version folders, but for now it just returns a hard-coded list for testing purposes.

After the function definitions, we proceed with lines that actually run the program. Lines 166 and 167 give us some handles to the UI Manager framework; ui links us to all of the available tools available with UI Manager, and disp is used to control the display of the windows and their interactivity. These are global variables, so they are visible inside the functions. The final three lines control the visibility of the windows. win:Show() makes the MainWindow visible. disp:RunLoop() allows the window to maintain interactivity—it continually monitors the state of the UI's components to detect changes. And once the loop has been broken, the final line hides the window.

  1.     local width,height = 1200,100
  2.     win = disp:AddWindow({
  3.         ID = 'VersionControl',
  4.         WindowTitle = 'Loaders Version Control',
  5.         Geometry = {2020, 100, width, height},
  6.  
  7.      
  8.         ui:VGroup{
  9.             ID = 'root',
  10.             ui:Tree{
  11.                 ID = 'Tree',
  12.                 SortingEnabled = true,
  13.                 Events = {
  14.                     ItemClicked = true,
  15.                 },
  16.             },
  17.         },
  18.     })

Back to the top we go to take a look at MainWindow(). Creating the layout for the GUI is done with the AddWindow() method. It gets just one argument, but that argument usually consists of a table with several sub-tables. UI Manager's Event handling requires the window to have an ID, so that's a required key in the table. The WindowTitle determines the name at the top of the window. Geometry determines the size and location of the window. I know there's a way to get a window to appear near the mouse, and I'll probably use it later on, but for now, the UI will always appear at the same location. Width and height will probably eventually be dynamic, dependent on the number and length of paths we're dealing with, so they've gone in as variables.

Now we come to the actual widgets that go into the window. In this case, we have only two elements: A VGroup, which serves as a container for everything else and has the ID root, and a Tree. A Tree is similar in appearance to a spreadsheet. We want to create an Event when an item is clicked, so we give the Tree an attribute Events and set ItemClicked = true. There is quite a bit more information about the different sorts of widgets that can go in a UI Manager window in the main UI Manager thread, and many examples can be found in the sample scripts available in Reactor.

  1.     -- Add your GUI element based event functions here:
  2.     itm = win:GetItems()
  3.  
  4.     -- Configure the Tree
  5.     itm.Tree.ColumnCount = 4
  6.  
  7.     itm.Tree.ColumnWidth[0] = 180
  8.     itm.Tree.ColumnWidth[1] = 60
  9.     itm.Tree.ColumnWidth[2] = 40
  10.     itm.Tree.ColumnWidth[3] = 300
  11.  
  12.     -- Add a header row
  13.     hdr = itm.Tree:NewItem()
  14.     hdr.Text[0] = "Loader Set"
  15.     hdr.Text[1] = 'Version'
  16.     hdr.Text[2] = 'Lock'
  17.     hdr.Text[3] = 'File Path'
  18.     itm.Tree:SetHeaderItem(hdr)
  19.  
  20.     -- Add Tree Rows (Replace with a call to PopulateLoaderTree)
  21.  
  22.     itRow = itm.Tree:NewItem()
  23.     itRow.Text[0] = 'mattePainting         (3)'
  24.     itRow.Text[1] = 'v03'
  25.     itRow.Text[2] = ''
  26.     itRow.Text[3] = 'D:/projects/foo/elements/mattePainting/v03/'
  27.     itm.Tree:AddTopLevelItem(itRow)
  28.  
  29.     itChild = itm.Tree:NewItem()
  30.     itChild.Text[0] = 'FG'
  31.     itChild.Text[1] = 'v03'
  32.     itChild.Text[2] = ''
  33.     itChild.Text[3] = 'D:/projects/foo/elements/mattePainting/v03/FG/201_030_mattePainting-FG_v03.exr'
  34.     itChild.CheckState[2] = "Unchecked"
  35.     itRow:AddChild(itChild)

With the window defined and its layout set, we query it for a list of its items with GetItems(). This fills the table itm with each widget's ID as a key. We can then access the various traits of the Tree using the address itm.Tree. Setting the header and adding rows works just as Andrew described in his thread, so I won't bother repeating it here. The bits that Andrew has yet to demonstrate are the use of the disclosure triangle to create nested rows and inserting a checkbox into one of the fields.

Most of the rows in a Tree are created with the method AddTopLevelItem(). In order to add a child under such a top level row, we need to keep track of the handle to the parent row, so we can't just use a loop like in Andrew's Tree demo—each row that goes in overwrites the handle, so we could only add child items to the very last row. That means that we'll need to either add the buffers before moving on to the next Loader Set or we'll need to keep a table of the Loader Set rows so that we can add children to them later. Either way will work, but we haven't gotten to that point quite yet. What I have here shows that itRow holds the results from NewItem(), and I use the AddChild() method on itRow to create the sub-item. I've also made itRow a global variable so that it can be modified from the other function.

The checkbox is very easy to add. Each column has a CheckState field, but if it doesn't contain either 'Checked' or 'Unchecked', the checkbox will not be displayed. Setting itChild.CheckState[2] = 'beef' has the same effect as not including the line at all—no checkbox.

I should mention also the SetTextColor() line. I haven't yet worked out how to use it, so that line doesn't actually do anything (as far as I can tell).

  1.     -- The window was closed
  2.     function win.On.VersionControl.Close(ev)
  3.         disp:ExitLoop()
  4.     end
  5.  
  6.     -- A tree row was clicked on
  7.     function win.On.Tree.ItemClicked(ev)
  8.         if ev.column == 1 then
  9.             ChooseVersion()
  10.         end
  11.     end

Next we have the event handling functions. ev is a special variable that contains information about what interactions the user has made. When the user clicks on a field in the Tree, the event function win.On.Tree.ItemClicked() activates. Since I want to only pop up the ChooseVersion window when the version field itself is clicked (not, for instance, when the Lock checkbox is changed), I have used an if statement that tests which for column 1. The function will be called no matter which field the user clicks, but it will do nothing if any field other than the version number was clicked.

If the user did click a version number field, then the ChooseVersion() function executes. Let's take a look at that one.

  1. function ChooseVersion()
  2.     local width,height = 250,45
  3.  
  4.     winVer = disp:AddWindow({
  5.         ID = 'versionWin',
  6.         WindowTitle = 'Choose New Version',
  7.         Geometry = {2050, 200, width, height},
  8.  
  9.         ui:VGroup{
  10.             ID = 'root',
  11.             ui:ComboBox{
  12.                 ID = 'VersionList',
  13.                 Text = 'version',
  14.             },
  15.         },
  16.     })
  17.  
  18.     -- Add your GUI element based event functions here:
  19.     itmVer = winVer:GetItems()
  20.  
  21.     versionList = populateVersionList()
  22.  
  23.     for i, j in ipairs(versionList) do
  24.         itmVer.VersionList:AddItem(j)
  25.     end
  26.  
  27.     function winVer.On.versionWin.Close(ev)
  28.         winVer:Hide()
  29.     end
  30.  
  31.     function winVer.On.VersionList.CurrentIndexChanged(ev)
  32.         index = itmVer.VersionList.CurrentIndex + 1
  33.         NewVersion = versionList[index]
  34.         itRow.Text[1] = NewVersion
  35.     end
  36.  
  37.     winVer:Show()
  38.  
  39. end

It is very similar to the previous function; this one uses a ComboBox widget instead of the Tree. Notice that I am using a different variable name to create the window and collect the item list. Since these variables are global in scope, I want to be sure that I don't accidentally overwrite them—I'll need to be able to access the MainWindow's item list later on, so those handles need to be preserved. As I mentioned earlier, I'm using a separate function to populate the list of versions, something that I'll need to do in MainWindow() as well if I want to keep the script well organized and easy to understand. When the ComboBox is changed, winVer.On.VersionList.CurrentIndexChanged() executes. Unlike most Lua tables, the ComboBox's indices start at 0 instead of 1, so I immediately add 1 to the index so that I don't get confused. I did a few iterations through getting the new version number when I was trying to understand what was going on, so the function has a line it really doesn't need. I could have just written it.Row.Text[1] = versionList[index] instead of passing it through another variable. At the end of the function, I call the window's Show() method to cause it to display. And let's rewind a bit to the Close command, where instead of exiting the loop, as is usually done, I simply hide this window. If I used disp:ExitLoop(), then the entire script would end instead of just closing the one window.

There's no reason to discuss populateVersionList() because it's just a placeholder for a more complicated procedure, so that concludes this look at the UI Manager prototype! Next time I'll import the code into the main script and start working out the functions I'll need to populate the lists.
Hi @Midgardsormr

First of all, good job messing around with reactor code enough to create your own little tool. I'm sure this tool can be of great help, but you did start something quite "serious" I would call it. What do I mean by that? Well strap in, here we go.

First of all it looks like you have parent/child items representing render layers with all their passes. It looks like they have a fixed structure on the file system as well.
This being said, if this really is your workflow and how you keep working, awesome. We can most definitely turn this tool into a great "pipeline" tool.
When you were talking about, the populateTree function, that would be the core of your interface, calling an amount of right-fit functions gathering and building data in such a format that you can just pass it to your user interface API and done, tree data filled.

So let's clarify some things, is the following path format assumption correct?
D:/projects/{PROJECT_CODE}/elements/{RENDER_GROUP/LAYER}/{VERSION}/{PASS}

If this structure is true, it means that we can create a utility function to actually build the tree row data from a path.
This also implies that to generate the complete tree data structure, we could call a function that parses the file system and uses above mentions function to build row data by path. So in the end you will have something like this:

# represents a root/top level node/item
{"mattePainting", "v03", false, "D:/projects/foo/elements/mattePainting/v03"}

# represents a child node/item
{"FG", "v03", false, "D:/projects/foo/elements/mattePainting/v03/FG/201_030_mattePainting-FG_v03.exr"}

As you can see both data sets are similar, due/thanks to their need to fit into the same tree view.
So let's go all the way and imagine that by building this data set, and passing it to a fillData style function, your tree will be populated.

(doesn't have to be in this json-like format, just to explicitly show you the data in a clear fashion)

Code: Select all

[
    {"root": {"layer": "mattePainting", "version": "v03", "locked": false, "path": "D:/projects/foo/elements/mattePainting/v03"}, 
     "children": [{"pass": "BG", "version": "v03", "locked": false, "path": "D:/projects/foo/elements/mattePainting/v03/BG/201_030_mattePainting-BG_v03.exr"},
                        {"pass": "FG", "version": "v03", "locked": false, "path": "D:/projects/foo/elements/mattePainting/v03/FG/201_030_mattePainting-FG_v03.exr"},
                        {"pass": "TECH", "version": "v03", "locked": false, "path": "D:/projects/foo/elements/mattePainting/v03/TECH/201_030_mattePainting-TECH_v03.exr"}}
]
This is the first goal to achieve I think, getting an API generic and easy to use enough (internally in your code) to do the following:
- get input data
- build row data from input data
- fill row data

So here is how far I have refactored your code right now:
  1. -- ===========================================================================
  2. -- constants
  3. -- ===========================================================================
  4. TREE_COLUMNS = {"Loader Set", "Version", "Lock", "File Path"}
  5.  
  6. -- ===========================================================================
  7. -- globals
  8. -- ===========================================================================
  9. _fusion = nil
  10. _comp = nil
  11.  
  12. -- ===========================================================================
  13. -- user interface
  14. -- ===========================================================================
  15. function createMainWindow(width, height, title)
  16.     -- creates the main window
  17.     window = disp:AddWindow({
  18.         ID = 'VersionControl',
  19.         WindowTitle = title,
  20.         Geometry = {100, 100, width, height},
  21.         ui:VGroup{
  22.             ID = 'root',
  23.             ui:Tree{
  24.                 ID = 'tree',
  25.                 SortingEnabled = true,
  26.                 Events = {
  27.                     ItemClicked = true,
  28.                 },
  29.             },
  30.         },
  31.     })
  32.     return window
  33. end
  34.  
  35. function configureTree(tree, columns)
  36.     -- configures given tree with given data
  37.     tree.ColumnCount = table.getn(columns)
  38.    
  39.     -- create column headers
  40.     local header = tree:NewItem()
  41.     for i, column in ipairs(columns) do
  42.         header.Text[i] = column
  43.     end
  44.     tree:SetHeaderItem(header)
  45. end
  46.  
  47. function setTreeItemValue(item, index, value, allowCheckbox)
  48.     -- set the value of a tree item
  49.     if type(value) == "boolean" and allowCheckbox then
  50.         -- if the type of value is bool, set the checkstate, if allowChecbox is true
  51.         item.Text[index] = ''
  52.         if value then
  53.             item.CheckState[index] = "Checked"
  54.         else
  55.             item.CheckState[index] = "Unchecked"
  56.         end
  57.     else
  58.         -- if not bool, set text
  59.         item.Text[index] = value
  60.     end
  61. end
  62.  
  63. function createTreeItem(tree, data, allowCheckbox)
  64.     -- creates a tree item
  65.     local item = tree:NewItem()
  66.     for i, value in ipairs(data) do
  67.         setTreeItemValue(item, i, value, allowCheckbox)
  68.     end
  69.     return item
  70. end
  71.  
  72. function addTreeTopLevelItem(tree, data)
  73.     -- creates a tree item parented to the tree
  74.     local item = createTreeItem(tree, data, false)
  75.     tree:AddTopLevelItem(item)
  76.     return item
  77. end
  78.  
  79. function addTreeLevelItem(tree, data, parent)
  80.     -- creates a tree item parented to the given parent item
  81.     local item = createTreeItem(tree, data, true)
  82.     parent:AddChild(item)
  83.     return item
  84. end
  85.  
  86. -- ===========================================================================
  87. -- functions
  88. -- ===========================================================================
  89. function getFusion()
  90.     -- check if global fusion is set. meaning this script is being
  91.     -- executed from within fusion
  92.     if fusion == nil then
  93.         -- remotely get the fusion ui instance
  94.         fusion = bmd.scriptapp("Fusion", "localhost")
  95.     end
  96.     return fusion
  97. end
  98.  
  99. function main()
  100.     -- get fusion instance
  101.     _fusion = getFusion()
  102.  
  103.     -- ensure a fusion instance was retrieved
  104.     if not _fusion then
  105.         error("Please open the Fusion GUI before running this tool.")
  106.     end
  107.  
  108.     -- get composition
  109.     _comp = _fusion.CurrentComp
  110.     SetActiveComp(_comp)
  111.  
  112.     -- load the bmd library
  113.     ldofile(fu:MapPath("Scripts:/bmd.scriptlib"))
  114.  
  115.     -- Set up aliases to the UI Manager framework
  116.     ui = fu.UIManager
  117.     disp = bmd.UIDispatcher(ui)
  118.  
  119.     -- create the main window
  120.     mainWindow = createMainWindow(1200, 100, 'Loaders Version Control')
  121.     mainWindowItems = mainWindow:GetItems()
  122.  
  123.     -- assign signals
  124.     -- close event
  125.     function mainWindow.On.VersionControl.Close(ev) disp:ExitLoop() end
  126.  
  127.     -- configure tree
  128.     configureTree(mainWindowItems.tree, TREE_COLUMNS)
  129.  
  130.     -- add items
  131.     parentItem = addTreeTopLevelItem(mainWindowItems.tree, {"mattePainting", "v03", false, "D:/projects/foo/elements/mattePainting/v03/"})
  132.     addTreeLevelItem(mainWindowItems.tree, {"BG", "v03", false, "D:/projects/foo/elements/mattePainting/v03/BG/201_030_mattePainting-BG_v03.exr"}, parentItem)
  133.     addTreeLevelItem(mainWindowItems.tree, {"FG", "v03", false, "D:/projects/foo/elements/mattePainting/v03/FG/201_030_mattePainting-FG_v03.exr"}, parentItem)
  134.     addTreeLevelItem(mainWindowItems.tree, {"TECH", "v03", false, "D:/projects/foo/elements/mattePainting/v03/TECH/201_030_mattePainting-TECH_v03.exr"}, parentItem)
  135.  
  136.     mainWindow:Show()
  137.     disp:RunLoop()
  138.     mainWindow:Hide()
  139. end
  140.  
  141. main()
I didn't reimplement the version window combo box (yet) because I haven't wrapped my head around how I want to structure that. Talking about structuring, usually these type of tools are built with a model/view (not MVC) design pattern. I'm sure we will be able to get this tool to a similar idea in the future. For now, let's just get it working, with some refactoring but not too prematurely.

If you have any questions, shoot, always there to help.
Hopefully this helps you out and isn't all too blurry for you @Midgardsormr.

Cheers
Cedric

User avatar
Midgardsormr
Fusionator
Posts: 1823
Joined: Wed Nov 26, 2014 8:04 pm
Answers: 15
Location: Los Angeles, CA, USA
Been thanked: 112 times
Contact:

Re: Building a Version Control script — collaborative learning

#30

Post by Midgardsormr » Fri Feb 16, 2018 5:35 pm

Thanks again, Cedric! I'll have to spend some time meditating on some of what you've done. Most of it makes sense to me, and I'm close enough to the rest that I think I can put it to good use, although it won't be seen in this message.

So let's clarify some things, is the following path format assumption correct?
D:/projects/{PROJECT_CODE}/elements/{RENDER_GROUP/LAYER}/{VERSION}/{PASS}

For the purposes of this exercise, yes. It's a little different in Muse's actual pipeline, so the script I eventually deploy at work will be a little different than the one I'm building here. I am hopeful that when all is said and done it can be made relatively easy to modify the script for other pipelines.



Back to the scripting:

At this point, realizing that always seeing all the Loaders is more useful than creating cases for different selections by the user, I've thrown away a good portion of the code. The centerpiece of the script now is the creation of a data structure to hold sets of related Loaders. As Cedric has described above, the process of building a data structure for each set of Loaders is the centerpiece of the script. As might be expected his solution is far more elegant than what I'm about to share, but part of the value of this thread, in my opinion, is the journey we're on. First I'd like to share the data structure I built:

  1. table: 0x005f89e8
  2.     1 = table: 0x005db770
  3.         1 = Loader (0x000000000EFF1410) [App: 'Fusion' on 127.0.0.1, UUID: c1997acd-9a9f-4aec-8bad-61401f653050]
  4.         2 = Loader (0x000000000EFF2520) [App: 'Fusion' on 127.0.0.1, UUID: c1997acd-9a9f-4aec-8bad-61401f653050]
  5. -- Many Loaders skipped for brevity's sake
  6.         119 = Loader (0x0000000034E15410) [App: 'Fusion' on 127.0.0.1, UUID: c1997acd-9a9f-4aec-8bad-61401f653050]
  7.         count = 119
  8.         element = lgt-cg
  9.         version = v02
  10.         pattern = X:\club\s02\203\02_shots\203_044_030\04_elements\renders\lgt-cg
  11.     2 = table: 0x0061a810
  12.         1 = Loader (0x000000000EFF9C90) [App: 'Fusion' on 127.0.0.1, UUID: c1997acd-9a9f-4aec-8bad-61401f653050]
  13.         count = 1
  14.         element = slowGoo
  15.         version = v10
  16.         pattern = x:\club\s02\203\02_shots\203_044_030\04_elements\precomp\slowGoo
  17.     3 = table: 0x005ea7d8
  18.         1 = Loader (0x0000000025B74F20) [App: 'Fusion' on 127.0.0.1, UUID: c1997acd-9a9f-4aec-8bad-61401f653050]
  19.         count = 1
  20.         element = 05_output
  21.         version = v04_comp
  22.         pattern = x:\club\s02\203\02_shots\203_044_010\05_output
  23.     4 = table: 0x005f0c48
  24.         1 = Loader (0x000000002B06A640) [App: 'Fusion' on 127.0.0.1, UUID: c1997acd-9a9f-4aec-8bad-61401f653050]
  25.         count = 1
  26.         element = OpticalFlow1
  27.         version = v10
  28.         pattern = x:\club\s02\203\02_shots\203_044_030\04_elements\precomp\OpticalFlow1
  29.     5 = table: 0x0060f128
  30.         1 = Loader (0x0000000034DFA980) [App: 'Fusion' on 127.0.0.1, UUID: c1997acd-9a9f-4aec-8bad-61401f653050]
  31.         count = 1
  32.         element = footprintsTrls
  33.         version = v06
  34.         pattern = x:\club\s02\203\02_shots\203_044_030\04_elements\precomp\footprintsTrls
  35.     6 = table: 0x00610af0
  36.         1 = Loader (0x0000000034DFCBA0) [App: 'Fusion' on 127.0.0.1, UUID: c1997acd-9a9f-4aec-8bad-61401f653050]
  37.         2 = Loader (0x0000000034E00FE0) [App: 'Fusion' on 127.0.0.1, UUID: c1997acd-9a9f-4aec-8bad-61401f653050]
  38.         count = 2
  39.         element = plates
  40.         version = v02
  41.         pattern = X:\club\s02\203\02_shots\203_044_030\04_elements\plates

This is a dump of an actual LoaderSets table from running the script on an active shot. I did not store the full path to the files because I am instead storing the Loaders themselves, and that information can easily be pulled out of the Loader. That, however, also led me to the peculiar choice to store some of the information I needed in the Loaders' CustomData instead of keeping it all in the table where it belongs.

That first entry in the table, with 119 Loaders, demonstrates a place where the script will need to be modified for Muse's pipeline. We have render layers (creature, goo, ear, shadows in this case) behind the version which are then broken out into buffers (diffuse, reflection, z, and so forth). When I adjust the script for our workflow, I will go one folder deeper to get that layer name.

My approach at this time is a two-step process. First, I get information about the Loader's path, then I use that information to construct the LoaderSets table.

  1. function getPattern(_tool)
  2.     -- Determine the directory delimiter used by the OS
  3.     local osSeparator = package.config:sub(1,1)    
  4.     -- Create a table out of the clip's path
  5.     local pathTable = bmd.split(_tool:GetInput("Clip"), osSeparator)
  6.  
  7.     local out = {}
  8.     local thisPattern = pathTable[1]                -- Variable to hold the pattern
  9.     local i = table.getn(pathTable)         -- an iterator for the loop
  10.     local version                           -- Variable to hold the version
  11.     local element
  12.  
  13.     -- Loop through entries in the pathTable, starting at the end
  14.     while i > 1 do
  15.         i = i - 1
  16.         if string.match(pathTable[i], '[Vv]%d+') ~= nil then        -- Match v##
  17.             version = pathTable[i]
  18.             element = pathTable[i-1]
  19.             break
  20.         elseif string.match(pathTable[i], '[Vv]_%d+') ~= nil then   -- Match v_##
  21.             version = pathTable[i]
  22.             element = pathTable[i-1]
  23.             break
  24.         end
  25.     end -- end of while
  26.  
  27.     -- Loop through the pathTable, constructing the pattern until we reach the version folder
  28.     for j=2, i-1 do
  29.         thisPattern = thisPattern .. osSeparator .. pathTable[j]
  30.     end
  31.  
  32.     -- Test to be sure we found a version folder, then return the pattern
  33.     if i > 1 then
  34.     else
  35.         thisPattern = _tool.Clip[1]             -- Oops! Do some error handling here.
  36.         version = ''
  37.         element = _tool.Name
  38.         c:Print("Version number not found for " .. _tool.Name .. ". \n")
  39.     end
  40.  
  41.     out.pattern = thisPattern
  42.     out.version = version
  43.     out.element = element
  44.  
  45.     -- Store the Loader's version in CustomData
  46.     _tool:SetData("version", version)
  47.  
  48.     return out
  49.  
  50. end -- end of GetPattern()

The procedure of acquiring the comparison pattern is pretty much the same as it was before, except that it's been removed to a function that takes a tool as input and returns a table, which will become part of an entry in LoaderSets. I realized while I was experimenting that if a Loader has been locked in a previous execution of the script that its version number will be out of sync with the rest of the layer, and it will therefore create its own Loader Set. That's not desirable, so when creating the comparison path, I've truncated before the version number instead. Instead of only returning the pattern, I have constructed a table that contains the pattern, version, and element name.

In the main() function, the second step iterates over every Loader found in the comp. The loop calls the getPattern() function and assigns its output to a variable thisPattern. Inside the loop is a second loop that iterates over LoaderSets, comparing thisPattern.pattern to the patterns that exist in the table. Where a match is found, the tool is added to the set subtable. If the pattern is not found, a new entry is placed in LoaderSets. There is nothing new or complex in terms of functions or syntax here:

  1.     local tools = {}                                -- Create a list to hold the Loaders
  2.     tools = c:GetToolList("false", "Loader")        -- Fill the list with all Loaders
  3.     LoaderSets = {}                                 -- Create a list to hold sets of related loaders
  4.  
  5.     -- Sort the Loaders into Loader Sets
  6.     -- see documentation of LoaderSets data structure at head of script
  7.  
  8.     -- For each Loader,
  9.     for i, tool in pairs(tools) do
  10.         --extract a pattern
  11.         thisPattern = getPattern(tool)
  12.         --For each previously extracted pattern
  13.         flag = 0
  14.         if LoaderSets[1] then
  15.             for i, set in ipairs(LoaderSets) do
  16.                 --if the new pattern matches,
  17.                 if thisPattern.pattern == set.pattern then
  18.                     --add this Loader to the associated Set
  19.                     table.insert(set, tool)
  20.                     c:Print("Inserted "..tool.Name.." into "..set.pattern.."\n")
  21.                     set.count = set.count + 1
  22.                     flag = 1
  23.                 end -- end of if pattern
  24.             end
  25.         end    
  26.  
  27.  
  28.         if flag == 0 then   -- Pattern not found, add new LoaderSet
  29.             table.insert(thisPattern, tool)
  30.             thisPattern.count = 1
  31.             table.insert(LoaderSets, thisPattern)
  32.             c:Print("Created "..thisPattern.pattern.."\n")
  33.         end
  34.     end -- end of for tools

Different comps may have vastly different numbers of Loaders in them, so I thought it would be nice if the window sized itself to accommodate the actual number of entries in the Tree. This line calculates the vertical size of the window: local windowSz = 50 + table.getn(LoaderSets) * 20. Then I call the MainWindow() function like so: MainWindow(100,100,900, windowSz).

Eventually, I'll grab information about the Fusion window location and use that to set the x and y coordinates instead of hard-coding them the way I've done here. I'm now populating the Tree with a function, so MainWindow() is leaner:

  1. function MainWindow(_x, _y, _width, _height)
  2.  
  3.     win = disp:AddWindow({
  4.         ID = 'VersionControl',
  5.         WindowTitle = 'Loaders Version Control',
  6.         Geometry = {_x, _y, _width, _height},
  7.      
  8.         ui:VGroup{
  9.                 ID = 'root',
  10.                 ui:Tree{
  11.                     ID = 'Tree',
  12.                     SortingEnabled = true,
  13.                 },
  14.         },
  15.     })
  16.  
  17.     -- Add your GUI element based event functions here:
  18.     itm = win:GetItems()
  19.  
  20.     -- Configure the Tree
  21.     itm.Tree.ColumnCount = 5
  22.  
  23.     itm.Tree.ColumnWidth[1] = 180
  24.     itm.Tree.ColumnWidth[2] = 60
  25.     itm.Tree.ColumnWidth[3] = 40
  26.     itm.Tree.ColumnWidth[4] = 300
  27.     itm.Tree.ColumnWidth[0] = 20
  28.  
  29.     -- Add a header row
  30.     hdr = itm.Tree:NewItem()
  31.     hdr.Text[1] = "Loader Set"
  32.     hdr.Text[2] = 'Version'
  33.     hdr.Text[3] = 'Lock'
  34.     hdr.Text[4] = 'File Path'
  35.     hdr.Text[0] = ''
  36.     itm.Tree:SetHeaderItem(hdr)
  37.  
  38.     -- Add Tree Rows (Replace with a call to PopulateLoaderTree)
  39.     setsList = populateLoaderTree()
  40.  
  41.  
  42.     -- The window was closed
  43.     function win.On.VersionControl.Close(ev)
  44.         disp:ExitLoop()
  45.     end
  46.  
  47.     -- A tree row was clicked on
  48.     function win.On.Tree.ItemClicked(ev)
  49.         if ev.column == 2 then
  50.             ChooseVersion(_x + 180, _y + 20, 240, 60, tonumber(ev.item.Text[0]))
  51.         end
  52.     end
  53.  
  54. end

Creating the window is the same as before, except that now all four of the Geometry attributes are controlled by arguments, and I've deleted the Events attribute—the only event I care about (at the moment) is ItemClicked, and that's the default behavior for a Tree.

I've added a column to hold a row index, which solves part of the issue with passing the chosen version from ChooseVersion() back to the Tree. I've hidden the new column from the user by making it narrow enough that only the disclosure triangle is visible, but not the index. It's hacky, but until we can figure out how to query which row the user clicked on, it's the best we can do.

Populating the tree is now dynamic and handled by the function populateLoaderTree():

  1. function populateLoaderTree()
  2.     list = {}
  3.     for i, set in ipairs(LoaderSets) do
  4.         list[i] = itm.Tree:NewItem()
  5.         list[i].Text[1] = set.element .. "     ("..set.count..")"
  6.         list[i].Text[2] = set.version
  7.         list[i].Text[4] = set.pattern
  8.         list[i].Text[0] = tostring(i)
  9.         itm.Tree:AddTopLevelItem(list[i])
  10.         for j, ldr in ipairs(set) do
  11.             child = itm.Tree:NewItem()
  12.             child.Text[0] = ldr.Name
  13.             if ldr:GetData("lock") == true then
  14.                 child.Text[2] = ldr:GetData("version")
  15.                 child.CheckState[3] = "Checked"
  16.             else
  17.                 child.Text[2] = set.version
  18.                 child.CheckState[3] = "Unchecked"
  19.             end
  20.             child.Text[4] = ldr.Clip[1]
  21.             list[i]:AddChild(child)
  22.  
  23.         end
  24.     end
  25.  
  26.     return list
  27. end

In order to preserve handles to each row of the Tree, I'm storing them in a table. LoaderSets is a global variable, so it's visible to the function even though it wasn't passed as an argument. Likewise, list is also global, which will make it visible to ChooseVersion(). I ought to capitalize that so I remember its scope… Since the parent-child relationship is implicit in the data structure, I can go ahead and add the child entries at the same time as I make the TopLevelItems.

It's not really necessary to return the list to MainWindow(), but I forgot to take that bit out.

Returning to MainWindow(), there's a change to the call to ChooseVersion(). I am now passing it coordinates relative to the main window and the row index of the item that was clicked.


There was only one change in ChooseVersion(). I can use the row index to target the specific entry in the Tree that I am changing: list[_row].Text[2] = versionList[index].

I still haven't written populateVersionList(), so that brings this entry to a close. Next time I'll dig into the operating system to get a list of versions currently on disk so we can fix that problem.