[Scripting] Opening a page in a web browser


Here’s a couple of ways to open a URL in a web browser from inside Softimage. Unfortunately, these worked on Windows only. On Linux, the JScript can’t create that ActiveX object, and the Python didn’t do anything.

JScript:

// Open a web page in the default browser
var objShell = new ActiveXObject("shell.application");
objShell.ShellExecute("http://support.solidangle.com", "", "", "open", 1);

Python:

import webbrowser
webbrowser.open( 'http://support.solidangle.com' )

Scripting: Getting the StrandPosition arrays


StrandPosition is an array of arrays: one array of positions for each strand.

Here’s a Python snippet:

from win32com.client import constants
xsi = Application

def dispFix( badDispatch ):
    import win32com.client.dynamic
    # Re-Wraps a bad dispatch into a working one:
    return win32com.client.dynamic.Dispatch(badDispatch)

attr = xsi.Selection(0).ActivePrimitive.Geometry.ICEAttributes( "StrandPosition" )
dataType = attr.DataType
data2D = attr.DataArray2D
for data in data2D:
   for elem in data:
      elem = dispFix(elem)
      xsi.LogMessage( "Vector3: " + str(elem.X) + ":" + str(elem.Y) + ":" + str(elem.Z) )

And here’s a JScript snippet:

a = Selection(0).ActivePrimitive.Geometry.ICEAttributes( "StrandPosition" );
LogMessage( ClassName(a) );

x = a.DataArray2D.toArray();
LogMessage( x.length );
for ( var i = 0; i < x.length; i++ )
{
   y = x[i].toArray();
   LogMessage( "=======================" );
   for ( var j = 0; j < y.length; j++ )
   {
      LogMessage( y[j].X + ", " + y[j].Y + ", " + y[j].Z );
   }
}

PPG callbacks and variable scope


The scope of variable b is this snippet. That is, the identifier b exists and has the value 1 only while this snippet is running. After that, it doesn’t exist unless some other code defines it.

Consequently, the OnClicked callback is going to fail with a ‘b’ is undefined error.

var myPset = ActiveSceneRoot.AddProperty("CustomProperty",false,"Mytest");
var myLayout = myPset.PPGLayout;

var b = 1;

function a_OnClicked(){
	LogMessage( b );	
}

myLayout.AddRow()
myLayout.AddButton("a","button");
myLayout.EndRow() 

myLayout.Logic=OnInit.toString() +  a_OnClicked.toString();
myLayout.Language = "JScript" ;

InspectObj(myPset);

The way it works is that Softimage creates a new instance of the JScript ActiveX scripting engine each time it needs to execute some fragment of code. So, the above snippet runs in one instances of the scripting engine (which is destroyed after the code is executed). Then later, when the button is clicked, the OnClicked callback runs in a new and different instances of the scripting engine. And that new instance of the scripting engine, there is no variable named b.

One way around this would be to add b as a parameter:

var myPset=ActiveSceneRoot.AddProperty("CustomProperty",false,"Mytest");
var b =	myPset.AddParameter2("b",siInt4,1,0,100,0,100,siClassifUnknown,siPersistable | siKeyable);

var myLayout=myPset.PPGLayout;

myLayout.AddRow()
myLayout.AddButton("a","button");
myLayout.EndRow() 
myLayout.Logic=OnInit.toString() +  a_OnClicked.toString();
myLayout.Language = "JScript" ;

InspectObj(myPset);

function a_OnClicked(){
	LogMessage(PPG.b);	
}

Another way would be to use a LogicFile, something like this:

// LogicFile for an on-the-fly custom property
var b = 1;
var c = -1;
function OnInit() {
	LogMessage( "OnInit" );
	c = 99;
}
function a_OnClicked(){
	LogMessage( b );	
	LogMessage( c );
}

Then your property would work like this:

var myPset=ActiveSceneRoot.AddProperty("CustomProperty",false,"Mytest");
var myLayout=myPset.PPGLayout;

myLayout.AddRow()
myLayout.AddButton("a","button");
myLayout.SetAttribute( siUILogicFile, "\\some\\path\\LogicFile.js" );

myLayout.EndRow() 
myLayout.Language = "JScript" ;

InspectObj(myPset);

And this would give the following output:

// INFO : OnInit
// INFO : 1
// INFO : 99

Finding shader nodes with no connections


Here’s a JScript snippet that finds render tree nodes that are not connected to anything in the render tree.

LogMessage( isConnected( Selection(0) ) );

function isConnected( o )
{
	var oDR = XSIUtils.DataRepository ;

	var strOpInfo = oDR.GetConnectionStackInfo( o )
//	LogMessage( strOpInfo );
	var oTopNode = ParseXML( strOpInfo ) ;

	var oConnections = oTopNode.childNodes ;
	if ( oConnections.length == 0 )
	{
		return false;
	}
	return true;
}

function ParseXML( strXML )
{
	var oXMLParser = new ActiveXObject("Microsoft.XMLDOM") 
	oXMLParser.async = false	
	oXMLParser.loadXML( strXML ) ;

	if (oXMLParser.parseError.errorCode != 0) 
	{
		logmessage( "Invalid XML " + oXMLParser.parseError.reason , siError ) ;	
		return null ;
	}

	// the xsi_file node
	// If this is NULL we must have failed to load the XML
	var oTopNode = oXMLParser.documentElement ;

	return oTopNode ;
}

Most of this JScript came from the SDK Explorer code in $XSI_HOME\Addons\sdkui\Application\Plugins\SDKExplorer.js, because I noticed that disconnected shaders would have an empty connection stack, and I didn’t want to go through all the parameters individually looking for connections.
SDK_Explorer_Connection_stack_is_empty

Here’s a Python version that does a little more: it follows the output connections to check whether or not the shader is ultimately connected the material.

# Python Code
import xml.etree.ElementTree as etree

def is_connected( o ):
	if not o.IsClassOf( 52 ):
		print "Input is not a shader"
		return False
		
	sXML = XSIUtils.DataRepository.GetConnectionStackInfo( o )
	root = etree.fromstring( sXML )
	for connection in root:
		if connection.find('type').text == 'out':
			x = Application.Dictionary.GetObject( connection.find('object').text )
			return True if x.IsClassOf( 64 ) else is_connected( x )
				
	return False

print is_connected( Application.Selection(0) )

Saturday Snippet: Selection, LAST, and SubComponents


Selection.Clear();
var oCube = ActiveSceneRoot.AddGeometry( "Cube", "MeshSurface" );

var n = oCube.ActivePrimitive.Geometry.Points.Count

//SelectGeometryComponents(oCube.Name + ".pnt[1,3,5,LAST]");
Selection.Add( oCube.ActivePrimitive.Geometry.Points(1) );
Selection.Add( oCube.ActivePrimitive.Geometry.Points(3) );
Selection.Add( oCube.ActivePrimitive.Geometry.Points(5) );
Selection.Add( oCube.ActivePrimitive.Geometry.Points(n-1) );

var sel = XSIFactory.CreateActiveXObject("XSI.Collection");           
sel.AddItems(Selection);                  

LogMessage( sel.Count );
// INFO : 1

LogMessage( ClassName( sel(0) ) );
// INFO : CollectionItem

LogMessage( sel(0).Value );
// INFO : cube.pnt[1,3,5,LAST]

n = sel(0).SubComponent.ComponentCollection.Count
LogMessage( sel(0).SubComponent.ComponentCollection(n-1).Index );
// INFO : 7

LogMessage( VBArray( sel(0).SubComponent.ElementArray ).toArray()[n-1] );
// INFO : 7

//LogMessage( sel(0).SubComponent.ComponentCollection );
LogMessage( VBArray( sel(0).SubComponent.ElementArray ).toArray() );

And don’t do this. You’ll have to restart Softimage to get selection working again.

var sel = XSIFactory.CreateActiveXObject("XSI.Collection");           
sel.AddItems(Selection);                  
sel(0).Value = "cube.pnt[7]";

Aha! Setting DataArray2D with JScript


It came to me this morning while I was lying in the dentist chair.
I almost had it last Thursday.
Hat tip: Ho Chung

//var o = Selection(0)
//var oICEAttrMats = o.ActivePrimitive.AddICEAttribute("MyString", siICENodeDataString, siICENodeStructureArray, siICENodeContextSingleton);


var o = Selection(0);
var a = o.ActivePrimitive.ICEAttributes("MyString");


sa = getSafeArray( [ "a", "b", "c" ] );
//jsa = new VBArray( sa ).toArray();

sa1 = getSafeArray( [sa] );

a.DataArray2D = sa1;

//
// Get a safearray from a JScript array
//
function getSafeArray(jsArr) {
    var dict = new ActiveXObject("Scripting.Dictionary");
    for (var i = 0; i < jsArr.length; i++)
    dict.add(i, jsArr[i]);
    return dict.Items();
}

Trying and failing to set DataArray2D with JScript


First, let’s use Python to set up two ICE attributes with DataArray2D.

si = Application
from win32com.client import constants as C            # win32com.client.constants

oObj = si.Selection(0)
a = oObj.ActivePrimitive.AddICEAttribute("MyString", C.siICENodeDataString, C.siICENodeStructureArray, C.siICENodeContextSingleton)
a.DataArray2D = [["a", "b", "c", "d", "e"]]

a1 = oObj.ActivePrimitive.AddICEAttribute("MyString2", C.siICENodeDataString, C.siICENodeStructureArray, C.siICENodeContextSingleton)
a1.DataArray2D = [["u", "v", "w" ]]

Now, let’s try to set DataArray2D with JScript. As a reminder, here’s how you access the DataArray2D in JScript:

o = Selection(0);
a = o.ActivePrimitive.ICEAttributes("MyString");
x = VBArray( a.DataArray2D ).toArray();
LogMessage( VBArray( x[0] ).toArray() );
// INFO : a,b,c,d,e

Seeing that, you would think that you could set DataArray2D using an array of arrays or maybe an array, but no:

a.DataArray2D =  [[ "a", "b", "c" ]];
// WARNING : 3390 - This ICEAttribute doesn't refer to a 2D array: <Attribute: MyString2>
// 

a.DataArray2D =  [ "a", "b", "c" ];
// WARNING : 3392 - Invalid offset specified while extracting data from this attribute: <Attribute: MyString2>
// <Offset: 110348408>
// 

At this point, I started wondering if there was anyway at all to do it, so I tried to put back the same value:

a.DataArray2D = a.DataArray2D;
// WARNING : 3393 - The input array doesn't match this attribute's data type or structure type: <Attribute: MyString2>
// 

Ack. Maybe if I converted it to a JScript array…well, at least something finally worked:

a.DataArray2D = VBArray( a.DataArray2D ).toArray();

Copying the DataArray2D from another attribute works too:

a = o.ActivePrimitive.ICEAttributes("MyString");
a1 = o.ActivePrimitive.ICEAttributes("MyString2");
a.DataArray2D = VBArray( a1.DataArray2D ).toArray();

So, based on that, I thought maybe I needed a safearray and things started getting a little hacky:

sa = getSafeArray( [ "a", "b", "c" ] );
jsa = new VBArray( sa ).toArray();

a.DataArray2D = sa;
// WARNING : 3392 - Invalid offset specified while extracting data from this attribute: <Attribute: MyString2>
// <Offset: 110348408>

a.DataArray2D = jsa;
// WARNING : 3392 - Invalid offset specified while extracting data from this attribute: <Attribute: MyString2>
// <Offset: 110348408>

//
// Get a safearray from a JScript array
//
function getSafeArray(jsArr) {
    var dict = new ActiveXObject("Scripting.Dictionary");
    for (var i = 0; i < jsArr.length; i++)
    dict.add(i, jsArr[i]);
    return dict.Items();
}

In summary, it doesn’t seem possible to set DataArray2D from JScript.

Setting the DataArray2D attribute in scripting


Last time I tried this, I gave up on JScript (it seemed impossible) and got something to work in Python.

In JScript, I kept getting errors like “# WARNING : 3392 – Invalid offset specified while extracting data from this attributeÈ.

si = Application
from win32com.client import constants as C            # win32com.client.constants

oObj = si.Selection(0)
oICEAttrMats = oObj.ActivePrimitive.AddICEAttribute("MyString", C.siICENodeDataString, C.siICENodeStructureArray, C.siICENodeContextSingleton)
oICEAttrMats.DataArray2D = [["a", "b", "c", "d"]]

x = oICEAttrMats.DataArray2D
print x
print len(x)
print len(x[0])
print len(x[0][0])

for d in x[0][0]:
    print d


# (((u'a', u'b', u'c', u'd'),),)
# 1
# 1
# 4
# a
# b
# c
# d

See also this Getting DataArray2D attribute values post.

Getting the DataArray2D for the Materials ICE attribute


Here’s the Python way:

Application.SelectObj("Pedestrian_Mesh.Actor_Copies", None, None);
o = Application.Selection(0)

a = o.ActivePrimitive.Geometry.GetICEAttributeFromName("Materials")
print len(a.DataArray2D)
print len(a.DataArray2D[0] )
print a.DataArray2D[0][0]
for s in a.DataArray2D[0][0]:
    print s

# 1
# 1
# (u'', u'Sources.Materials.PedestrianLib.Shoes', u'Sources.Materials.PedestrianLib.Hair', u'Sources.Materials.PedestrianLib.Legs', u'Sources.Materials.PedestrianLib.Skin', u'Sources.Materials.PedestrianLib.Shirt')
# Sources.Materials.PedestrianLib.Shoes
# Sources.Materials.PedestrianLib.Hair
# Sources.Materials.PedestrianLib.Legs
# Sources.Materials.PedestrianLib.Skin
# Sources.Materials.PedestrianLib.Shirt

And here’s how to do it in JScript:

o = Selection(0);

a = o.ActivePrimitive.Geometry.GetICEAttributeFromName("Materials");

x = new VBArray( a.DataArray2D ).toArray();
y = new VBArray( x[0] ).toArray();
for ( var i = 0; i < y.length; i++ )
{
    LogMessage( y[i] )
}

Saturday Snippet: Selecting a range of polygons by polygon index


Given a selected polygon, add the next 5 polygons to the selection. For example, if polygon 22 is selected, then add polygons 23,24,25,26, and 27 to the selection.

You can do this without using any loops, by using a string expression for the polygon components.

Here’s some JScript that shows two ways to do it. Method 1 uses a string expression. Method 2 is a loop, which can either use the Polygons collection or a string expression.

var o = Selection(0).SubComponent.Parent3DObject
var p = Selection(0).SubComponent.ComponentCollection(0)
var LAST = o.ActivePrimitive.Geometry.Polygons.Count;

// Method 1
// SelectGeometryComponents("grid.poly[22-27]", null, null);

var sPoly = ".poly["+p.Index+"-"+Math.min( p.Index+5,LAST-1 )+"]"
//SelectGeometryComponents( o.FullName + sPoly );
//Selection.SetAsText( o.FullName + sPoly  );

// Method 2
//for ( var i = p.Index+1 ; i < Math.min( p.Index+5,LAST ) ; i++ )
//{
//	ToggleSelection( o.ActivePrimitive.Geometry.Polygons(i) );
// -or-
//	ToggleSelection( o.FullName + ".poly["+i+"]" )
//}

Here’s some Python that does something similar but different:

si = Application
o = si.Selection(0).SubComponent.Parent3DObject
p = si.Selection(0).SubComponent.ComponentCollection(0)
LAST = o.ActivePrimitive.Geometry.Polygons.Count

s = ".poly[%s-%s]" % ( p.Index, min( p.Index+5,LAST-1 ) )
si.SelectGeometryComponents( s )