Browsing"3dsMax"

Floating Dialog Amnesty

Mar 16, 2010 by     9 Comments    Posted under: 3dsMax, DotNet, Maxscript, Tips and Tricks

WindowBoxUI.png

Ack, not another one of those posts where the only picture is a p*ss poor dialog with some stolen twinkly twink icons. My old art teacher would be despairing right now, and perhaps comforting himself by grinding a yellow ochre paste. Oh, how he loved that yellow ochre.

A situation existed recently where between working on the train and at work, I’d lost some dialogs on the screen space outside the resolution of my laptop monitor. A few of 3ds max’s window locations are stored in the ini file.

inipath = getMAXIniFile()
setINISetting inipath “MtlEditorPosition”  “MainWindow” “0 0 375 734”
setINISetting inipath “RenderVFBPosition”  “Position” “0 0”
setINISetting inipath “MaterialBrowserDialogPosition” “Dimension” “0 0 340 600”
setINISetting inipath “EnvironmentDialogPosition” “Dimension” “0 0 350 580”
setINISetting inipath “RenderDialogPosition” “Dimension” “0 0 360 750”
setINISetting inipath “mentalrayMessagesPosition” “Dimension” “0 0 600 400”
setINISetting inipath “RenderPresetsCategoryDialogPosition” “RenderPresetsCategoryDialogDimension” “0 0 210 280”

But most of the useful ones are not – like the layer manager and curve editor.

Unfortunately, you cant set their position like you can a rollout floater – it meant it was time for the programmatic equivalent of James Herriot putting on a large rubber glove.

Windows has a wealth of useful functions in the API that aren’t yet exposed to the dotnet framework – well not out the box, there is a managed API but I haven’t looked into this yet . However this doesn’t mean you cant use native API calls in a managed code environment. You can declare an API call in a dotnet assembly, and with a little coercion, get it working as you want.

pinvokelogoThe place to look for Win32 and API related stuff is pinvoke.net. This has loads of info about how to call these windows functions via the managed dotnet framework

From the site itself-

“The term PInvoke is derived from the phrase “Platform Invoke”. PInvoke signatures are native method signatures, also known as Declare statements in VB”

After looking on this site, I found the methods i was looking for.

  • GetWindowRect – Gets the size of the window from the window handle
  • GetWindowPlacement – Gets the current window location from the window handle
  • MoveWindow – Moves the window to a screen location

All that was need now was to convert these signatures into a format string ready to compile directly from 3dsmax, so that I could call these win32 functions from a dotnetobject within 3dsmax, and hopefully retrieve my missing windows.

When you compile a dll directly in 3dsmax, you are taking the code that you would write within the Visual Studio window and sending it to the compiler manually. So the flavour of managed language you are using dictates the compiler type.

The key to this class is to set up some custom properties so that the data coming out when used in 3dsmax is not too weird –  it is logical to return a drawing.point for a location, and a drawing.size for a window size for instance.

Getwindowrect uses a RECT structure (a structure is similar to a class) but creating one in max can be tricky. Mike Biddlecombe on CG Talk had posted a method to instantiate a structure in 3dsmax by adding it into an array and retrieving the first member. That was brilliant stuff and typical of the sort of stuff that Mike is figuring out the whole time. I decided in this case, that since I was compiling an assembly anyway, i’d just handle all of the structure business there and avoid any potential banana skins. So I added a width and height property to the RECT structure definition, meaning after it was instantiated, you can call the method from PInvoke and return a structure that has standard properties. Planning return values in your classes is something that helps in the long run – It makes deployment much easier in Max. I have found that since the VS interface is easier to debug, it’s a good place to do as much as you can there.

fn DialogWindowOpsClass =
(
source = “”
source += “Imports System.Runtime.InteropServicesn”
source += “Imports System.Drawingn”
source += “Public Class DialogWindowOpsn”
source += “Public Structure RECTn”
source += “Public left As Integern”
source += “Public top As Integern”
source += “Public right As Integern”
source += “Public bottom As Integern”
source += “Public ReadOnly Property Width() As Integern”
source += “Getn”
source += “Return right – leftn”
source += “End Getn”
source += “End Propertyn”
source += “Public ReadOnly Property Height() As Integern”
source += “Getn”
source += “Return bottom – topn”
source += “End Getn”
source += “End Propertyn”
source += “End Structuren”
source += “Public Structure POINTAPIn”
source += “Public x As Integern”
source += “Public y As Integern”
source += “End Structuren”
source += “Public Structure WINDOWPLACEMENTn”
source += “Public Length As Integern”
source += “Public flags As Integern”
source += “Public showCmd As Integern”
source += “Public ptMinPosition As POINTAPIn”
source += “Public ptMaxPosition As POINTAPIn”
source += “Public rcNormalPosition As RECTn”
source += “End Structuren”
source += “<DllImport(“user32.dll”)> _n”
source += “Public Shared Function MoveWindow(ByVal hWnd As System.IntPtr, ByVal x As Integer, ByVal y As Integer, ByVal nWidth As Integer, ByVal nHeight As Integer, ByVal bRepaint As Boolean) As Booleann”
source += “End Functionn”
source += “<DllImport(“user32.dll”)> _n”
source += “Public Shared Function GetWindowRect(ByVal hWnd As System.IntPtr, ByRef lpRect As RECT) As Booleann”
source += “End Functionn”
source += “<DllImport(“user32.dll”)> _n”
source += “Public Shared Function GetWindowPlacement(ByVal hWnd As System.IntPtr, ByRef lpwndpl As WINDOWPLACEMENT) As Booleann”
source += “End Functionn”
source += “Public Function WindowSize(ByVal Hwnd As System.IntPtr) As System.Drawing.Sizen”
source += “Dim LPRECT As RECTn”
source += “GetWindowRect(Hwnd, LPRECT)n”
source += “Dim WinSize As System.drawing.size = New System.drawing.size(LPRECT.Width, LPRECT.Height)n”
source += “Return WinSizen”
source += “End Functionn”
source += “Public Function WindowPosition(ByVal Hwnd As System.IntPtr) As System.Drawing.Pointn”
source += “Dim intRet As Integern”
source += “Dim wpTemp As WINDOWPLACEMENT = New WINDOWPLACEMENT()n”
source += “wpTemp.Length = System.Runtime.InteropServices.Marshal.SizeOf(wpTemp)n”
source += “intRet = GetWindowPlacement(Hwnd, wpTemp)n”
source += “Dim WinPoint As System.drawing.point = New System.drawing.point(wpTemp.rcNormalPosition.left, wpTemp.rcNormalPosition.top)n”
source += “Return WinPointn”
source += “End Functionn”
source += “End Class”

VBProvider = dotnetobject “Microsoft.VisualBasic.VBCodeProvider”
compilerParams = dotnetobject “System.CodeDom.Compiler.CompilerParameters”
compilerParams.ReferencedAssemblies.add “C:WindowsMicrosoft.NETFrameworkv2.0.50727System.drawing.dll”
compilerParams.GenerateInMemory = on
compilerResults = VBProvider.CompileAssemblyFromSource compilerParams #(source)

— this is very useful to debug your source code and check for referencing errors
if (compilerResults.Errors.Count > 0 ) then
(
errs = stringstream “”
for i = 0 to (compilerResults.Errors.Count-1) do
(
err = compilerResults.Errors.Item[i]
format “Error:% Line:% Column:% %n” err.ErrorNumber err.Line
err.Column err.ErrorText to:errs
)
MessageBox (errs as string) title: “Errors encountered while compiling VB code”
return undefined
)
return compilerResults.CompiledAssembly.CreateInstance “DialogWindowOps”
)

What this code does is make a class that can be instantiated in max and used to control the positions of the dialogs in the 3dsmax user interface, whether we can see them or not.

In order to call them though, we need an array of handles for the active max dialogs. Fortunately, Maxscript gives us a way of doing this –

UIAccessor.GetPopupDialogs()

The rest, was building an interface. I went for the datagridview – Its a complex user control and I am probably only scratching the surface of the sort of things you can do. Its designed for handling masses of complex data from data sources like sql databases but you can just use it like a listview. Its just got a few more options.

Maxscript also has methods for interacting with win32 functions, so it’s entirely possible that this could be achieved using that, but then that’d just be cheating, wouldn’t it?

download

Clear out the dead wood from the layer manager

Mar 16, 2010 by     2 Comments    Posted under: 3dsMax, Maxscript, Tips and Tricks

Occasionally, you end up with a load of layers in your scene that are no use to anybody, serve no function to society, and end up taking valuable space like an unwanted uncle at a wedding.

layermanager

Here is a macro that will check the layer manager interface and remove any layer that is empty.

Macroscript ClearEmptyLayers 
category:"LoneRobot" 
tooltip:"Clear Empty Layers" 
buttontext:"Clear Layers" 

(   
DeletedLayerCount = 0 
local deflayer = layermanager.getlayer 0 
deflayer.current = true
for i = Layermanager.count-1 to 1 by-1 do
	(  
		layer = layermanager.getLayer i  
		local thislayername = layer.name 
		layer.nodes &theNodes  
		if thenodes.count== 0 then (LayerManager.deleteLayerbyname thislayername;DeletedLayerCount +=1)
	) 
	if not DeletedLayerCount == 0 then Messagebox ("Number of layers removed - " + DeletedLayerCount as string) title:"Layer Manager" 
)

And after –

layerafter

If only it was that easy to remove Piers Morgan from ‘the scene’

One thing to note is how the layermanager interface retrieves the contents of the layer –

if thenodes.count== 0 then …

The ampersand before the variable declares an array to be used and populates it with the layer contents. You might also notice that it deletes the layers in reverse order, since each time you delete one, the layer count becomes less. So if you deleted it going forward you would probably reach a point where the loop integer would be higher than the number of remaining layers, boo.

You also have to minus one from the total count as the default layer is located at 0.

Supressing 3dsMax hotkeys in composite control assemblies

Sep 15, 2009 by     No Comments    Posted under: 3dsMax, DotNet, Maxscript, Tips and Tricks

The way to use a dotnet textbox on a max rollout is to use the gotfocus and lostfocus events. This allows you to know when the textbox has been highlighted for text entry, and use the enableaccelerators command to temporailly suspend the max hotkeys.

rollout tbtest “MXS Dotnet texbox” width:227 height:23
(
dotnetcontrol textboxctrl “TextBox” pos:[1,1] width:225 height:16

on textboxctrl gotfocus sender arg do enableacellerators = false
on textboxctrl lostfocus sender arg do enableacellerators = true
)
createdialog tbtest

In my previous article, I detailed a method that can automatically set the backcolor of a custom control using the managedservices.dll . A similar principle could be applied to the textbox. If you were building a custom control, you could override the gotfocus and lostfocus methods and use the managedservices dll to call the maxscript function. Then you have a control that has this functionality built in. It’s really simple, you just write a control that inherits the textbox and add the functionality you need. The VB code is below –

Public Class MaxTextBox
Inherits System.Windows.Forms.TextBox

Protected Overrides Sub OnGotFocus(ByVal e As System.EventArgs)
ManagedServices.AppSDK.DisableAccelerators()
End Sub

Protected Overrides Sub OnLostFocus(ByVal e As System.EventArgs)
ManagedServices.AppSDK.EnableAccelerators()
End Sub
End Class

Again, the solution is simple to implement in maxscript, so this would be perfectly okay. However, if you wanted to build a composite control that contained a textbox, you would be stuck with the same problem. The code above would be all you would need to add. You would use the maxtextbox inside a composite control, If you added this class to your solution in visual studio, you could drag the new inherited control and add 3dsMax functionality natively to the assembly.

I should mention that this method is only available in 3dsMax 2010

FloatSpinner – A custom control to bypass 3dsmax value casting issues

James Haywood of Bungie Studios recently pointed out an issue on CGTalk regarding the 3ds maxscript/dotnet conversion functions. Notably it was to do with a decimal from dot net being cast into and integer in 3ds max. This could present a problem if you are using a numericupdown control in max (This is the spinner equivalent in Dot net)

James noticed that any decimal values were automatically being cast into the nearest integer. If you check the MXS help, it does indeed support this fact.

From the list, it seemed that what was needed in order for this to function in 3dsmax properly was a spinner control that returned a dotnet single as it’s event handler rather than the current decimal. This is possible. You may have read the other articles about control inheritance and custom events. The new control will be using the same principles.

Control Inheritance

It’s the one form of inheritance that the treasury isn’t thinking of taxing people on. We inherit the numericupdown control, as it already contains the functionality we need in our control, (like how we use extends in MXS). The numericupdown becomes the base class of the control.In VB you refer to this by typing MyBase. This is similar to the way you can use delegate in a scripted plugin.

Once this is done, we have to override the properties we need to change. In VB, the rough syntax for setting up a property that stores any form of information within the assembly is as follows –

private _QuantumTheory As String
<Category(“Data”), Description(“Something that nobody understands”)> _
Public Property QuantumTheory() As String
Get
Return _QuantumTheory
End Get
Set(ByVal value As String)
_QuantumTheory = value
End Set
End Property

You’ll see that the variable is stored within a private member which is the same, except for an underscore before it. Properties are basically a function that has a get and set block. Get, in this case is called when you type something like control.QuantumTheory. It simply returns whatever is stored within the private variable _QuantumTheory. Set is called when you type something like control.QuantumTheory = “unproven”. This is the basic layout for a property the only one simpler than this is a readonly property, that do not have a set part. However, you can put whatever you like within the set part to perform error checking and options for the user. For example, you might want to check a supplied value isn’t outside the maximum or minimum range of the control, like on a spinner. Without some error checking here, you would get an exception thrown if a user tried to set this property. So you could in fact extend the to property to perform logical comparisons on the value submitted.

Another thing you can do is add information about the property. Before the property declaration, you can provide a category and description of the property. This can help organise things better, and viewing the control in Visual studio will show all categories together and provide information about the property and it’s usage. This isn’t always necessary with properties like value, and backcolor, but some properties might need clarifying. I’ll add that I’m not brilliant at doing this myself – Most of the time if I make a control, it’s just for something I’m doing so I miss this bit out if I’m in a rush. (This has been done on the XML panel download assembly though)

The properties that need to be changed are the ones that previously returned a decimal as it’s return type. You’ll see on the property declaration above that all elements are declared with the AS operator. This determines that the property returns whatever type specified. In the case of the floatspinner, here is the value property –

Shadows Property Value() As Single
Get
Return _Value
End Get
Set(ByVal value As Single)
MyBase.Value = value
_Value = Math.Round(CSng(MyBase.Value), MyBase.DecimalPlaces)
End Set
End Property

It is declared as shadows in VB as you are using a property that exists on the base class that you want to replace. I don’t know if this is the correct or proper way to do this in VB but is the only way I have found so far. The thing to note this time is that the property returns a Single. This will mean that when this property is retrieved in 3dsmax, it will be in the correct data type for max to cast into a float, rather than the decimal to integer conversion it currently performs.

Be a DotNet Pirate – Use an Event Arrrrg

This isn’t the only thing that needs to be done. Once the properties have been overridden, the control needs to provide an event for the data to be given to 3dsmax. I have talked in the past about custom event arguments, and they are a great way to make you controls perform tasks where you get exactly the information you need out of the control, in the format ready to be used without further casting or conversion. This is recapping other posts, but is really important if you want to start to develop your own controls for max. Custom Event arguments are classes themselves, so also have properties. It is these properties that allow you to query the eventargs for the information that you want. So in the standard mouse event args, there are stored properties for that particular mouse movement that allow you to get information about the pointer –

System.Windows.Forms.MouseEventArgs Properties –
e.Button
e.Clicks
e.Delta
e.Location
e.X
e.Y

We are going to write a simple eventarg that returns a value as a single in the form of an event property. This way, we know that 3dsMax will get the correct data structure we need.

Public Class LoneRobotValueChangedEventArgs
Inherits EventArgs
Private _Value As Single
Public Property Value() As Single
Get
Return _Value
End Get
Set(ByVal value As Single)
_Value = value
End Set
End Property
Public Sub New(ByVal Value As Single)
Me.Value = Value
End Sub
End Class

This is pretty much it, all we need to add to the custom control is the connection between the valuechanged event in the numericupdown, and our new event class. We do this by overriding that, and calling our new event class. firstly, we override the event we want to alter –

Public Shadows Event ValueChanged(ByVal sender As Object, ByVal e As LoneRobotValueChangedEventArgs)

We have changed it so that it now accepts a lonerobotValueChangedEventArgs instead of a SystemValueChangedEventArgs

Protected Overrides Sub OnValueChanged(ByVal e As System.EventArgs)
Me.Value = Math.Round(CSng(MyBase.Value), MyBase.DecimalPlaces)
RaiseEvent ValueChanged(Me, New LoneRobotValueChangedEventArgs(Math.Round(CSng(MyBase.Value), MyBase.DecimalPlaces)))
End Sub


You’ll see that when we raise the event, we do so by instantiating a new lonerobotValueChangedEventArgs object. The sender (i.e the control) is the first argument, and a second argument which is a number that is set into the event property “value”. Now that has been performs, you can retrieve this value in max as a single and therefore get the float value that you need.

Download the source for this below. As I mentioned before in the post, I haven’t had a great deal of time to write this properly, perhaps someone could add properties if needed and post the results.

download script

Appendix

I also mentioned in the forum that I had seen a class in the managed services dll called maxspinner.

I couldn’t instantiate this so I asked over at the SDKblog on the Area. This was the response that I got –

Regarding your .NET Questions.

First off, a disclaimer – if a class doesn’t have specific documentation (in this class, it’s excluded) then we don’t officially support its usage.

That said, those classes are public (because they need to be used across Assembly boundaries) and we can’t really prevent anyone from at least trying. 🙂

MaxSpinner is a wrapper class that takes the Spinner custom control that we expose in Win32 and provides a managed interface so that it can be used in an interop solution. This means that we take care of instantiating the class, but the client would need to grab the handle, using MaxNativeControl::HostWindowHandle, and embed it in an interop solution (such as System.Windows.Interop.HwndHost in WPF.)

So there you are, that clears that up. MaxSpinner is not for us mortals.

Follow Up –

DotNet Supremo Yannick Peuch has managed to clear up this issue with a conversion before Max casts to and integer. Thanks for sharing this Yannick!

(
rollout upDownRolloutTest “NumericUpDown Control Test” width:220 height:45
(
dotNetControl upDownCtrl “System.Windows.Forms.NumericUpDown” pos:[10,10] width:50 height:25
dotNetControl btnGetValue “System.Windows.Forms.Button” pos:[90,10] width:100 height:21
on upDownRolloutTest open do
(
upDownCtrl.DecimalPlaces = 1
upDownCtrl.Increment = 0.1
upDownCtrl.Value = 1.0
upDownCtrl.Minimum = 0.0
upDownCtrl.Maximum = 10.0
btnGetValue.Text = “Get Value”
btnGetValue.FlatStyle = (dotNetClass “System.Windows.Forms.FlatStyle”).System
)
on btnGetValue MouseClick do
(
— Get the value auto converted by MAXScript
fValue = upDownCtrl.Value
format “Wrong Value : %n” fValue
— Get the decimal value as a .NET object
dValue = getProperty upDownCtrl #value asDotNetObject:true
— Convert it to single object value then auto converted to float value by MAXScript
fValue = (dotNetClass “System.Decimal”).ToSingle dValue
format “Good Value : %n” fValue
createDialog upDownRolloutTest
)

Generating PDF Documents via MXS and .NET

Apr 14, 2009 by     No Comments    Posted under: 3dsMax, DotNet, Imaging, Maxscript, Program Automation, Technical Research

Recently, I discovered an open source library written in C# called PDFSharp. This came about because I was working on a script that generated a text file with information in at the end. Unfortunately, when it came to refining the presentation, my ASCI skills are seriously limited. For example, my attempt at the LoneRobot logo looked like this –

............o
............|
......... ___
........ /../
......8=/- /=8  - BeepBeepDoWopDoDoopDeep!
....... ---
........| |
........o.o

In the magazine “ASCI Art Lovers Monthly” , this attempt gained a four asterisk rating, although others have subsequently informed me that the **** peppered over the artistic comments page are not related to this.

Because this generated document was to be printed and used as reference, It got me thinking about whether I could generate better printable documents directly from 3dsMax without having to use OpenOffice, Acrobat or any of my presentation skills. Fortunately, PDFSharp is an assembly that allows you to create PDF documents. The way it does this is to mirror a whole set of GDI+/WPF drawing classes and allow you to construct the page programmatically via DotNet. (If you want to look at a few DotNet GDI+ drawing methods for 3dsmax, check out this page)

Regardless of it’s limited appeal within Max, it is an amazing piece of open source code that should most certainly be looked at.

In order to integrate this, I wrote a small MXS Struct to wrap a few of the functions of PDFSharp’s GDI+ drawing methods, since this was all I was interested in using for my purposes. There are many, many more drawing and formatting options available through this assembly, but in the struct the following methods are available –

  • DrawImage (With optional scale multiplier)
  • DrawRectangle (Specify a corner radius to draw a rounded rectangle)
  • DrawEllipse
  • DrawLine (With different line styles)
  • DrawString (With various paragraph alignment options – Left,Right,Justify,Center)

The PDF is drawn on a GDI+ drawing surface, in this case a GDI bitmap the size of an A4 document. There is a basic example within the test code that creates the following PDF file –

Don’t Forget, you’ll need to replace the line where it loads in and draws the image, or the TestDocument function will fail.

You can get PDFSharp from here, and of course download the example below as normal. It’s well commented so you should be able to get an idea of how to use it. The source code of PDFSharp has a pretty comprehensive array of test projects to pick at.

I’m still waiting for our subscription copy of max 2010, so I’ll probably add a version later using the private/public declarations available in this release.

If anyone is interested, the text on the PDF is from the rather beautiful short story ‘The Man Who Planted Trees‘ by Jean Giono.

download script