Browsing"DotNet"

The camera sometimes lies

Aug 11, 2010 by     1 Comment     Posted under: DotNet

Just a snippet posting this time, sharing a small file callback to lock and unlock a camera’s position. If you need to make sure that a camera in a file doesn’t get moved when it is handled by a different artist, this could be useful. Locking a camera is simple, all you need to do is set the transform lock flags on, and (Thanks to Jason Labbe for this suggestion on CGTalk)  lock the FOV of the camera with a script controller.

Added to a file open callback, you can lock all cameras when a file is opened. I have deployed this on the fix team’s computers as a callback, that way nothing gets moved after animation and the subsequent edit. There are a couple of macros to lock and unlock too, should this need to be altered at any time, but the main idea is that it avoids accidental ‘nudging’ of camera positions between file versions which can be easily done.

Unlocked

cams unlocked

Locked!

cams locked

The callback just sits in a file the script startup directory –

callbacks.removeScripts id:#CamLock   
callbacks.addScript #filePostOpen "macros.run "LoneRobot" "LockAllCameras"" id:#CamLock

And the macro is as follows –

macroScript LockAllCameras
category:"LoneRobot"
toolTip:"Lock all Scene Cameras"
buttontext:"Lock Cams"
(
	if cameras.count > 0 then
	(
	For cam in cameras do
		(
		setTransformLockFlags cam #all
		cam.wirecolor = orange
			if classof cam == TargetCamera then
			(
			LockScript = Float_Script()
			cam.fov.controller = lockscript
			)
		)
	-- check that it isn't the target - checking fot the type property is a good enough way as any
	Format "Camera lock applied to % cameras - %n" (for c in cameras where hasProperty c "type" collect c).count maxFileName
	)
)

macroScript UnlockAllCameras
category:"LoneRobot"
toolTip:"Unlock all Scene Cameras"
buttontext:"Unlock Cams"
(
	if cameras.count > 0 then
	(
		For cam in cameras do
		(
		setTransformLockFlags cam #none
		cam.wirecolor = (color 87 120 204)
			if classof cam == TargetCamera then
			(
			UnlockScript = Bezier_Float()
			cam.fov.controller = Unlockscript
			)
		)
	Format "% cameras unlocked - %n" (for c in cameras where hasProperty c "type" collect c).count maxFileName
	)
)

if someone else asks “Why did you choose orange?”, I’m going on a rampage.

Stuff the revolution, I’m thinking about a Character Selection Framework

Jun 1, 2010 by     10 Comments    Posted under: 3dsMax, Characters, DotNet, Rigging, Technical Research, User Controls

 

One of the most time consuming things always seems to be building pesky UI’s in Maxscript. The ever reliable Visual Maxscript editor, whilst having the reputation of Marmite, has been compromised by a spectacular bug in the last few releases that prints the last few characters of the previous line onto the next one, thus scrambling your code.

rollout MXSMashup "Untitled" width:169 height:171
     (
         button btn1 "Button" pos:[7,8] width:70 height:39
     39
         bitmap bmp1 "Bitmap" pos:[97,25] width:52 height:84
    84
        checkbutton ckb1 "CheckButton" pos:[28,138] width:132 height:20
    20
        combobox cbx1 "ComboBox" pos:[16,91] width:55 height:1
    1
        edittext edt1 "" pos:[14,60] width:55 height:20
    20
    )

All this means you end up manually  tweaking positions and sizes till the end of time, for a script  that is essentially doing something very simple. It’s always faster to have a small dialog with a picture of the rig with fast access to individual bones to remove the necessity of picking them in the viewport. However, generating these things up can usually take more time than we have to give in production. I thought it would be an interesting focus to my research to see how easy it would be to implement a system that allowed even a non-scripter to build specialised dialogs for selection, ultimately a task that you end up doing a billion times with a rig.

The IDE driven usercontrol approach

Now Visual Studio has a pretty nifty IDE, and you can layout windows forms pretty darned fast with it. It would be rather handy to have something similar but centred around 3dsMax, albeit with a basic layout toolset. The obvious approach (in terms of linking the dotnet framework to 3dsMax objects) was binding controls to the names of scene nodes. Okay, maybe scene node names might not be the most robust of solutions but it is fine for this sort of thing – but if someone starts renaming rigs just for fun, you’re going to be screwed on a bigger level than just the selection tools.

Look at this lot –

all dialogs

With Rig Studio, you can do this without any coding in no time at all. Here’s how –

A Runtime Design Surface

Have a look at the video below to see an example of how RigStudio sets up a basic control dialog and then stores it to be read by a user end interface.

You build up an interface by drag and drop, and there are preset sizes and shapes defined for speed. You can size these up to whatever you like. There are options to set the forecolor and background color, and to position text blocks to illustrate the different areas of the dialog.

You might recognise the control on the right of the UI – It’s a propertygrid and is a great little control for exactly this type of thing, as it takes a dotnetobject and displays an area so that you can adjust the properties easily. You can use this out of the box, but I have customised it to only display the properties that you need to adjust. I have also written a custom UITypeEditor class to handle the control list drop down method for choosing hierarchies.

TypeEditor

This is not a default behaviour of the propertygrid control. the Child property on a rigcontrol object is a string denoting the name of the next node in the hierarchy. In order to give better design-time support, I am asking the UITypeEditor  to take an array of controls from the design surface and build a sorted listbox from the results. It needs to be sorted as the controls could have been created in any order.

Custom Toolstrip Renderers

Alignment-Options_Dotnet Alignment-Options

Keen-eyed UI aficionados will notice the similarity of the drop down menu on the left to 3ds Max’s menu system, and not the default dotnet toolstrip renderer (which is on the right). Its a subtle difference, but this is due to the use of a custom toolstrip renderer. Autodesk has provided this as part of the MaxCustomControls namespace.

MaxToolStripSystemRenderer is a custom renderer and is assigned by simply giving an instance of it to the renderer property. So you can implement this into Maxscript pretty easilly too, if you want everything to look like it is part of the same application.

-- VB
Me.CtrlMenu.Renderer = New MaxCustomControls.MaxToolStripSystemRenderer()
-- MXS
CtrlMenu.Renderer = dotnetobject "MaxCustomControls.MaxToolStripSystemRenderer"

What might annoy you about the toolstrip, especially if you run the UI from the dark side of the force, is the border that windows paints around the toolstrip menu. This is just plain annoying if you are trying to keep a nice minimal looking UI.

toolstrip

It’s probably not so noticeable on a light UI scheme, but it would be great to get rid of it. With some poking around on some VB forums, I found this solution. There is a protected method called OnRenderToolstripBorder. All that you need to do is to override this and tell it to do nothing, as you don’t want the border. The great thing about class inheritance is that we can inherit the custom renderer class made by Autodesk, implement the functionality we need, and have a new class to use. By only overriding the methods that we want to change, we keep all the other existing render bits. It is actually a pretty short entry to have a version of the Autodesk Toolstrip renderer without the white border line –

Public Class BorderlessMaxToolStripRenderer
Inherits MaxCustomControls.MaxToolStripSystemRenderer
	Protected Overrides Sub OnRenderToolStripBorder _
				(ByVal e As System.Windows.Forms.ToolStripRenderEventArgs)
 	'Do nothing
 	End Sub
End Class

So now you can have the great functionality of the toolstrip without having an incongruous looking UI. It would be pretty easy to dynamically compile this class as part of a maxscript, so that you dont have to distribute an assembly too.

Toolstrip-CustomRenderer

Weirdly, you don’t get this border with a menustrip, which is fine for most things, but it’s not got the same choice of controls. Its a shame i didnt realise this until I had gone down this route!

Design-TIme Alignment Functions

Controls have a left, right, top and bottom property which returns their position relating to their parent container. This is a local position and unrelated to screen position. This makes the majority of options in the alignment menu relatively easy. Where it becomes more complicated is control spacing. Controls can be added in any order, so you need a method of deciding where a control is in dialog position terms. If you are performing a spacing arrangement, you need a method that can take an array of controls and return them sorted into order by their location property. As you can space vertically or horizionally, you’ll need a way of tells the method the direction that you want to sort them. It is precisiely this sort of logic that the Icomparer can handle, sorting abstract classes according to  defined properties.

Fortunately, the dotNet framework has some powerful sorting method called Icomparer for doing this job.

Public Enum AlignmentCompareOptions
    Vertical
    Horizontal
End Enum

Friend Class PointComparer
    Implements IComparer(Of Drawing.Point)
    Dim _CompareDirection As AlignmentCompareOptions

    Public Sub New(ByVal Direction As AlignmentCompareOptions)
        _CompareDirection = Direction
    End Sub
    Public Function Compare(ByVal x As Drawing.Point, ByVal y As Drawing.Point) As Integer Implements IComparer(Of System.Drawing.Point).Compare
        Dim pointX As Point = DirectCast(x, Point)
        Dim pointY As Point = DirectCast(y, Point)

        Select Case _CompareDirection
            Case AlignmentCompareOptions.Vertical
                Select Case True
                    Case pointX.Y > pointY.Y
                        Return 1
                    Case pointX.Y < pointY.Y
                        Return -1
                    Case pointX.Y = pointY.Y
                        Return 0
                End Select
            Case AlignmentCompareOptions.Horizontal
                Select Case True
                    Case pointX.X > pointY.X
                        Return 1
                    Case pointX.X < pointY.X
                        Return -1
                    Case pointX.X = pointY.X
                        Return 0
                End Select
        End Select

    End Function

End Class

In order to use this class, you add the controls to a SortedDictionary, one of the classes that you can supply an Icomparer to the constructor. You’ll see this SortedDictionary takes a point value, and a control value. As these are added the Sorted dictionary automatically sorts them into the order they are according to the direction (vertical or horizontal)

Dim CompareControlList As New SortedDictionary(Of Point, Control)(New PointComparer(Direction))

The Icomparer sorts it by returning an integer that relates to the evaluated expression within the IComparer, 1 if pointA is greater than pointB, –1 if B is greater than A, and 0 if they are the same.

The conclusion to all this is that you can use the results to calculate the difference between the first and last control instances, deduct the combined height or width of the controls and divide by the number of gaps to get the distance needed to space the controls evenly.

XML Control Serialization

This was a new thing for me to get my head around, and in doing so I genuinely believe I have only scratched the surface on the sort of things that you can do. In my case, XML serialization provided an elegant solution to this problem –

XMLfile.ChildNodes.ItemOf(i).ChildNodes.ItemOf(i).ChildNodes.ItemOf(i).InnerText

Sometimes, negotiating an XML tree manually becomes difficult to track, but in this case, you explicitly know what type of information you are dealing with. XML serialization allows you to deconstruct dotnetobjects, with the idea that you can use the serializer’s logic to re-assemble them at another time, perhaps even to send data to a compliant application on a different computer.

To use the example of, say a bog-standard button, we could serialize this but we have to address a couple of issues first. XML needs a specific data type to store the information. You couldn’t just pass it an enum style like “borderstyle.fixedsingle” as a string and expect it to know what to do with it. Also, there are a sh*t load of properties that we have on a button, many of which don’t particularly represent anything to do with the visual state. You would be serializing a lot of useless information into the XML file, making the file larger than it needs to be.

Writing a Custom Serialization class

I ended up by writing a class specifically to handle the serialization of the design surface to XML. It actually consists of three classes, the RigControlSerializer is where the action happens. It stores all the dialog information necessary to recreate the size and shape of the stored data. RigControl and RigControlText are subclasses that allow the serializer to loop through the controls on the design surface and store them into an array list. So the serializer property controlist actually has an array of the RigControl class, not the actual control itself, but is perfect as it is all the information that the deserializer needs to recreate the control faithfully on another surface.

Custom Serializer Classes

The great thing about writing a serializer class is that each property becomes a branch in the xml file automatically,  and subsequent paths are made as each property is serialized in order. So an entire dialog can be written out to XML as follows –

Private Function SerializeDesignSurfacetoXML(ByVal XMLFilename As String) As Boolean

        Try
            Dim SavedDialog As RigControlSerializer = New RigControlSerializer()

            With SavedDialog
                .DialogSize = DesignSurface.Size
                .DialogBackcolor = (CType(DesignSurface.BackColor, Color)).ToArgb
                .ControlList = CreateRigControlCopyArray()
                .TextLabelList = CreateTextLabelCopyArray()
                .BackgroundImage = CType(Me.DesignSurface.BackgroundImage, System.Drawing.Bitmap)
                .CharacterName = Me.CharacterName
            End With

            Dim writer As New XmlSerializer(GetType(RigControlSerializer))
            Dim file As New StreamWriter(XMLFilename)
            writer.Serialize(file, SavedDialog)
            file.Close()
            writer = Nothing
            Return True

        Catch ex As Exception
            Return False
            MessageBox.Show(ex.InnerException.ToString)
        End Try
    End Function

That, for me is a pretty straightforward way of converting a whole bunch of dotnet objects into an XML file!

As you control the headings of the XML branches via the class, its pretty easy to see what is going on in the XML file itself. As much as it seems like more work, I think it is a quite elegant method.

xml

One other useful thing to crop up with this is Binary Serialization. You use the same serializer class to dump all the control positions to a temporary .dat file. Why is this useful? You’ve just used the serializer to introduce an undo buffer, in case the control alignment didn’t happen as you expected, or as I do pick the wrong direction to align.

The RigSelector User Control

RigSelectorVS

The ultimate payoff to the IDE approach is providing a simple way to rebuild the selection dialog from the XML data. The RigSelector usercontrol is just a composite control that only really features a panel. The key is having a method to deserialize the XML to populate the panel with the stored node hierarchy.

We have reused the BorderlessAutodeskToolstripRenderer that we used earlier for the button.

It can resize the parent form to the dimensions of the stored dialog control.

RigSelectorClass

The rest is done by adding the handlers when deserializing the control to fire the selection within max. This perhaps the simplest part. To fire Maxscript code from within a dotnetclass you use –

>ManagedServices.MaxscriptSDK.ExecuteMaxscriptCommand(<<MXS_String>>)

Lastly, my best piece of advice about implementing ANY form of custom dotnet control class in 3dsMax.

Don’t be shy about calling enable accelerators

to pass focus back to your max shortcuts!

As much as the maxform control is supposed to handle all this stuff, using a custom class can put a spanner in the works, and there’s nothing worse than realising that you’ve just knocked out your hotkeys.

ManagedServices.AppSDK.EnableAccelerators()

I call this after every button click, which might be overkill but it’s better than losing focus to your dotnetcontrol and being powerless to get them back without a restart or frenzied maxscript call. Any one wanting to do this sort of thing should really look into ManagedServices.dll, there is some great stuff in there.

Polar-DesignSurface

I hope this article has been of interest! Comments are always welcome!

Rig Studio Update!

I recently added automatic rig generation to Rig Studio – This speeds things up considerably.

I have also added a new control – A layer control. This allows you to store a name string of a corresponding layer. This has hide and unhide functionality build in, so you can control character rig and mesh visibility without having to open the layer manager.

Shameless Plug Alert! – HitchHiker is being used at Volition Inc

Mar 31, 2010 by     No Comments    Posted under: DotNet, News, User Controls

It’s not often that you get an email from a maxscript legend like Jeff Hanna so I did have to do a double take at my inbox when it arrived. But as it transpired, Jeff had been looking to update one of the pipeline tools at games company Volition Inc.

Previously, the control used had been an ActiveX thumbnailer, but since the swap to X64 architecture, this had been made redundant. So Jeff informed me that he’d been looking for a dotnet alternative and wanted to use Hitchhiker. I implemented a couple of functionality requests and VPropShop4 is the result!

vPropShop_HitchHiker

VpropShop is an asset manager used at Volition Inc. It features a beta release of Hitchiker with added maxfile support. It is also able to capture a viewport thumbnail directly from the assembly, passing the maxscript call directly to max and then returning it as a dotnet image.

Thanks to Jeff for letting me use this, It’s always nice to find out the tools on LoneRobot.net are useful and helping artists and animators get their jobs done faster, giving them more time to do the really interesting stuff.

If you have done anything with Hitchiker (or another example of LR.net) whether its a simple viewer or a full asset browser please let me know, I’d love to post it.

There will be an update to Hitchhiker shortly showing the features above, plus enhanced thumbnail caching, and custom wildcard search parameters.

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

Adding a post build event to Visual Studio

Mar 16, 2010 by     4 Comments    Posted under: DotNet

If you are developing dotnet tools for use in 3ds studio max, you will be used to a painfully slow method of evaluating your assemblies in max, unless you do something clever with dynamic compiling. Since the assembly cannot be unloaded after loading it you are forced to exit max, copy the assembly to the dll location and start max again.

I have sped this up slightly by adding a post build event to Visual studio, which means that on a successful build of the assembly, it automatically copies the dll to my Lonerobot folder (which is where I load all my custom assemblies from)

The procedure is pretty straightforward –

Right click the solution in the solution explorer

solutionwindow

Click Compile>Build events (the button at the bottom)

compile

In the textbox marked ‘Post build event command line’, I enter –

COPY "$(TargetPath)" "C:Program FilesAutodesk3ds Max 2010ScriptsLoneRobotClassLib"

postbuild

This will obviously be different for your assembly. This command should now copy the compiled assembly to the correct directory. Not revolutionary, but helpful me thinks.