Rollout and Struct Pairs

From Tech Artists Wiki

Jump to: navigation, search

A very good way to organize MAXScripttools is by pairing them with structs. The problem specifically addressed by this article is getting rid of globals. Please see that article for a full explanation of why globals shouldn't be used.

Contents

Example Script

Consider the following script, which is one tool consisting of two rollouts:

rollout ro_test1 "Test 1"
(
	local var
	button btn_a "A"
 
	on btn_a pressed do
		print (var + var)
 
	on ro_test1 open do
	(
		var = timeStamp()
		print (var + var)
	)
)
 
rollout ro_test2 "Test 2"
(
	local var
	button btn_b "B"
 
	on btn_b pressed do
		print (var + var)
 
	on ro_test2 open do
	(
		var = ro_test1.var
		print (var + var)
	)
)
 
fl = newRolloutFloater "Test" 200 200
addNewRollout ro_test1 fl
addNewRollout ro_test2 fl

Issues

The problems here are not all immediately obvious. First problem is, what we really want to do is get some value when the rollout is initialized- var- and use it across both rollouts. However, we are instead grabbing it once, and then assigning ro_test2's to ro_test1's. This can create problems since we cannot guarantee what will be initialized when, and causes hidden bugs (it will crash if ro_test1 is added to fl in a closed state, for example).

Second problem is, this doesn't make sense. We want var defined for both rollouts, but it logically it doesn't belong to ro_test1 anymore than it does to ro_test2. We cannot set var externally from the rollouts, because they need to use it when they are opened.

Third problem is, the execution is contained within the event handlers. Yes, this can be fixed by putting the event handler code into functions contained in the rollout, but this is a bad solution, since it is equally poor for batching or external access (the rollout must exist, and be initialized/set up properly to do stuff).

The above is known as bad code design. It is a common affliction but there is a cure.

Solutions

There are really two solutions.

Globals (BAD!)

The following code would work:

global var
fn addAndPrint val =
	print (val + val)
 
rollout ro_test1 "Test 1"
(
	button btn_a "A"
 
	on btn_a pressed do
		addAndPrint var
 
	on ro_test1 open do
	(
		addAndPrint var
	)
)
 
rollout ro_test2 "Test 2"
(
	button btn_b "B"
 
	on btn_b pressed do
		addAndPrint var
 
	on ro_test2 open do
	(
		addAndPrint var
	)
)
 
var = timeStamp()
fl = newRolloutFloater "Test" 200 200
addNewRollout ro_test1 fl
addNewRollout ro_test2 fl

It works, but is no real improvement. We have two globals now: var and addAndPrint (and fl would be a third). Why hasn't it improved?

  • Yes we've solved the first issue. But now our code is much more confusing. We are defining things used inside the rollout from outside of the rollout, at a certain spot. This brings up the problems described in the globals article.
  • var's initialization still doesn't make sense. The code is equally irrational, just now in a different spot (and less likely to crash).
  • We've moved the code out of the event handlers, and into a global function. For our purposes, this works better than putting it in the rollout (since it'd have to reference another rollout, or be duplicated), but still contains all the problems in the globals article. It would have been evil to use 'var' inside of the function- instead, we will pass it as an argument from the event handlers.

Structs (GOOD!)

For a much better solution, you'd end up with the following code:

struct testDef
(
	var = timeStamp(),
	fn addAndPrint = print (var + var)
)
global test
 
rollout ro_test1 "Test 1"
(
	button btn_a "A"
 
	on btn_a pressed do
		test.addAndPrint()
 
	on ro_test1 open do
	(
		test.addAndPrint()
	)
)
 
rollout ro_test2 "Test 2"
(
	button btn_b "B"
 
	on btn_b pressed do
		test.addAndPrint()
 
	on ro_test2 open do
	(
		test.addAndPrint()
	)
)
 
test = testDef()
fl = newRolloutFloater "Test" 200 200
addNewRollout ro_test1 fl
addNewRollout ro_test2 fl

Why Structs?

So admittedly the cleaned up code doesn't provide a huge gain. And rightfully so, since it is a very simple situation. However, as the example tool at the end will show, it can greatly simplify tools where various parts (rollouts, tool clauses, etc) need to work together.

For the discussion, let's consider this sample script. It is a pared down tool from work that demonstrates the benefits of using structs. The real tool is more complex in lots of ways but this demonstrates the fundamentals I think:

struct rigUtilsDef
(
	pNode,
	pNodeMod = pNode.modifiers["Puppet Tools"],
	lvanimlist = pNodeMod.ps_time_rol.lvanimlist,
	animNodes = puppet.getAnimationNodes pNode,
	--and a pretty advanced initialization routine
	boneNodes = getAllChildren pNode,
	fn isHub a = (matchPattern ((classOf a.baseobject) as string) pattern:"*HUB*"),
	fn isDigit a = classOf a.baseobject == puppetShop_digitNode,
	hubNodes = for a in boneNodes where isHub a collect a,
	digitNodes = for a in boneNodes where isDigit a collect a,
 
	fn parent =
	(
		local oldState = PS_CONTROLLERLOCKSTATE
		PS_CONTROLLERLOCKSTATE = false
		for i = 1 to childSet.count do
		(
			childSet[i].parent = parentSet[i]
		)
		PS_CONTROLLERLOCKSTATE = oldState
	),
 
	fn unparent =
	(
		local oldState = PS_CONTROLLERLOCKSTATE
		PS_CONTROLLERLOCKSTATE = false
		for a in childSet do
			a.parent = undefined
		PS_CONTROLLERLOCKSTATE = oldState
	),
 
	fn getLeftRightCenterArraysFromPos =
	(
		rightArr = for a in boneNodes where a.pos.x < -.005 collect a
		leftArr = #()
		for a in rightArr do
		(
			--test against each rightArr member's mirror pos across x
			for b in boneNodes do
			(
				if (closeEnoughXYZ (a.pos * [-1,1,1]) b.pos .005) then
				(
					append leftArr a
				)
			)--for b
		)
		centerArr = #(pNode)
		for a in boneNodes where not arrops.isInArr rightArr a and not arrops.isInArr leftArr a do
			append centerArr a
		if rightArr.count == leftArr.count then
			true
		else
			false
	),
 
	fn posRightToLeft =
	(
		if (getLeftRightCenterArrays()) then
		(
			for i = 1 to rightArr.count do
			(
				leftArr[i].pos = rightArr[i].pos * [-1, 1, 1]
			)
		)
		else messageBox "Rig is not symmetrical"
	)
)
global rigUtils
 
 
rollout ro_rigUtils "Rig Utils"
(
	local btnWidth = 130
	button b_posRL "Copy Positions R->L" width:btnWidth
	button b_hubAsChild "Hub" width:(btnWidth/2) across:2
	button b_digitAsChild "Digit" width:(btnWidth/2)
	button b_unparent "Unparent" width:btnWidth
	button b_parent "Parent" width:btnWidth
 
	on b_unparent pressed do
	(
		rigUtils.unparent()
	)
 
	on b_parent pressed do
	(
		rigUtils.parent()
	)
 
	--Set the child set
	on b_hubAsChild pressed do
	(
		rigUtils.initSets rigUtils.hubNodes
	)
	on b_digitAsChild pressed do
	(
		rigUtils.initSets rigUtils.digitNodes
	)
 
	on b_posRL pressed do
	(
		rigUtils.posRightToLeft()
	)
 
	on ro_pTools close do
	(
		rigUtils.parent()
	)
)
 
if selection.count == 1 then
(
	global rigUtils = rigUtilsDef pNode:selection[1]
	createDialog ro_rigUtils
)

Structs for tools gives you the following:

  • Auto-initialize common variables:
  • Common variables not needed as arguments:

Example Tool

code here
Personal tools