March 27, 2009

This post is one of the winning entries of the F# programming contest started at the beginning of the year. It was submitted by an old friend of mine, Qun Lu, who also happens to be a member of the AutoCAD engineering team, and makes use of a new API in AutoCAD 2010: the somewhat ominously-named Overrule API.

The Overrule API is really (and I mean really, really) cool. Yes, I know: another really cool API in AutoCAD 2010? Well, I’m honestly not one to hype things up, but I do have a tendency to get excited by technology that has incredibly interesting capabilities with a relatively low barrier of entry. And the Overrule API is one of those APIs. It’s the answer to the question posed in this previous post, which raises concerns about translating the power and complexity of custom objects to the world of .NET:

So what’s the right thing to do? Clearly we could just go ahead and expose the mechanism as it is today in ObjectARX. And yet here we are with a technology we know to be highly complex and difficult to implement, and an ideal opportunity to redesign it – enabling more people to harness it effectively at lower effort. The more favoured approach (at least from our perspective) would be to investigate further how better to meet developers’ needs for enabling custom graphics/behaviour (a.k.a. stylization) in AutoCAD – in a way that could be supported technically for many releases to come.

The Overrule API allows you to hook into the display and other aspects of the behaviour of entities inside AutoCAD. The below example is a great example: when enabled, the code overrules the display of lines and circles, to make them into coloured pipes. And all with very little code (which would also be true if the code were in C# or VB.NET).

Here’s the F# code:

#light

module DrawOverrule.Commands

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.Geometry

open Autodesk.AutoCAD.GraphicsInterface

open Autodesk.AutoCAD.Colors

typepublic DrawOverrule public () as this =

inherit DrawableOverrule()

staticmemberpublic theOverrule =

new DrawOverrule()

staticmemberprivate Radius = 0.5

memberprivate this.sweepOpts = new SweepOptions()

override this.WorldDraw (d : Drawable, wd : WorldDraw) =

match d with

// Type-test and cast. If succeeds, cast to "line"

| :? Line as line ->

// Draw the line as is, with overruled attributes

base.WorldDraw(line, wd) |> ignore

if not line.Id.IsNull && line.Length > 0.0 then

// Draw a pipe around the line

let c = wd.SubEntityTraits.TrueColor

wd.SubEntityTraits.TrueColor <-

new EntityColor(0x00AfAfff)

wd.SubEntityTraits.LineWeight <-

LineWeight.LineWeight000

let clr =

new Circle

(line.StartPoint, line.EndPoint-line.StartPoint,

DrawOverrule.Radius)

let pipe = new ExtrudedSurface()

try

pipe.CreateExtrudedSurface

(clr, line.EndPoint-line.StartPoint, this.sweepOpts)

with

| e -> printfn("Failed with CreateExtrudedSurface")

clr.Dispose()

pipe.WorldDraw(wd) |> ignore

pipe.Dispose()

wd.SubEntityTraits.TrueColor <- c

true

| :? Circle as circle ->

// Draw the circle as is, with overruled attributes

base.WorldDraw(circle, wd) |> ignore

// needed to avoid ill-formed swept surface

if circle.Radius > DrawOverrule.Radius then

// draw a pipe around the cirle

let c = wd.SubEntityTraits.TrueColor

wd.SubEntityTraits.TrueColor <-

new EntityColor(0x3fffe0e0)

wd.SubEntityTraits.LineWeight <-

LineWeight.LineWeight000

let normal =

(circle.Center-circle.StartPoint).

CrossProduct(circle.Normal)

let clr =

new Circle

(circle.StartPoint, normal, DrawOverrule.Radius)

let pipe = new SweptSurface()

pipe.CreateSweptSurface(clr, circle, this.sweepOpts)

clr.Dispose()

pipe.WorldDraw(wd) |> ignore

pipe.Dispose()

wd.SubEntityTraits.TrueColor <- c

true

| _ ->

base.WorldDraw(d, wd)

override this.SetAttributes (d : Drawable, t : DrawableTraits) =

let b = base.SetAttributes(d, t)

match d with

| :? Line ->

// If d is LINE, set color to index 6

t.Color <- 6s

// and lineweight to .40 mm

t.LineWeight <- LineWeight.LineWeight040

| :? Circle ->

// If d is CIRCLE, set color to index 2

t.Color <- 2s

// and lineweight to .60 mm

t.LineWeight <- LineWeight.LineWeight060

| _ -> ()

b

let Overrule enable =

// Regen to see the effect

// (turn on/off Overruling and LWDISPLAY)

DrawableOverrule.Overruling <- enable

match enable with

| true-> Application.SetSystemVariable("LWDISPLAY", 1)

| false-> Application.SetSystemVariable("LWDISPLAY", 0)

let doc =

Application.DocumentManager.MdiActiveDocument

doc.SendStringToExecute("REGEN3\n", true, false, false)

doc.Editor.Regen()

// Now we declare our commands

[<CommandMethod("overrule1")>]

let OverruleStart() =

// Targeting all Drawables, but only affects Lines and Circles

ObjectOverrule.AddOverrule

(RXClass.GetClass(typeof<Drawable>),

DrawOverrule.theOverrule, true)

Overrule(true)

[<CommandMethod("overrule0")>]

let OverruleEnd() =

Overrule(false)

Here’s what happens when we load the application, turn the overrule on using the OVERRULE1 command (OVERRULE0 is the command to turn the overrule off – it’s details like this that tell you Qun’s a real programmer… ;-) and draw some lines and circles:

Even in a 3D view – this time with the realistic visual style applied – you get the piping effect when you draw simple geometry:

To be clear: these are standard AutoCAD lines and circles. When you use the OVERRULE0 command to disable the overrule, they revert to their original form:

I expect to follow this post – in time – with various more harnessing the power of this very cool API. If you have questions or ideas about how it might be used, be sure to post a comment.

Thanks & congratulations, Qun! Your copy of “Expert F#” is on its way to you via inter-office mail. :-) More soon on the other winning entry…

March 25, 2009

A big thanks to Stephen Preston, who manages DevTech Americas and coordinates our worldwide AutoCAD workgroup as well as spending time working with the AutoCAD Engineering team (phew!), for providing this sample. Stephen originally put it together for our annual Developer Days tour late last year: I took the original sample, converted it from VB.NET to C# and made some minor changes to the code. The VB.NET version is available from the ADN website, in case.

The Free-Form Design feature in AutoCAD 2010 is one of the coolest enhancements to the product (I really like the Parametric Drawing feature, too, although as the API for that is currently C++-only it’s unfortunately going to get a little less air-time on this blog). This post looks at how to automate free-form design operations: starting with a traditional Solid3d object, converting it to a sub-division mesh (SubDMesh) which we then manipulate in a number of interesting ways before converting back to a Solid3d.

Here’s what happens when we run the CC command, selecting this solid and applying the recommended values (0.2 – i.e. 20% – for the inflation percentage and 0.05 – i.e. 5% – for the maximum random bump). I selected a number of edges and faces at random after which the code assigned random colours to all the faces, so please don’t get frustrated trying to reproduce these exact results. :-)

The objects are (going from right to left):

The original solid.

A sub-division mesh created by inflating the original solid and applying creases to certain edges and extruding certain faces.

A traditional AutoCAD solid created from the sub-division mesh.

The best way to understand the overall functionality of the application is to scan the CC command – defined by the CreateCasing function found at the bottom of the listing – and then looking into individual functions it uses. The application also defines another command – SPHM – which shows how to create a simple, spherical sub-division mesh.

March 23, 2009

After getting my feet wet in the last post with my first IronPython application running inside AutoCAD, I decided it was time to attack a slightly more challenging problem: jigging a database-resident Solid3d object. The idea had come after I’d received a question by email from David Wolfe, who wanted to have a fully rendered 3D view of a cylinder he was jigging.

I’d done something similar for a prototype application I worked on late last year (which was demoed at AU). The jig itself only collected the selection data I needed – the display of the Solid3d objects was handled either via the Transient Graphics subsystem or by modifying database-resident Solid3d objects (which were interconnected by a separate system of relationships and constraints). But anyway, the point is that only when the Solid3d objects were database-resident could I get rendered graphics to be generated for them via either the conceptual or realistic visual styles.

Which is why it occurred to me that the technique shown in this recent post for jigging db-resident blocks with attributes might also apply here, too.

And, just for fun, why not do the whole thing in Python? (Actually, which hindsight I can now think of a lot of reasons… :-)

Part of my rationale behind attempting this was that we were going to have to derive our jig class from EntityJig and make sure the appropriate methods were overridden for AutoCAD to then call our jig at the right moments. This is something I had doubts about being able to do, given my previous experience. IronPython is surprisingly good at allowing these methods to be implemented and called dynamically – something that I expect will grow on me – but the downside was that with the PYLOAD integration we used in the last post it is very hard to tell why things aren’t working. It took me a number of hours to work out that an __init__ function was needed – one which took an Entity and passed it to the constructor of the base class, EntityJig – and that without it the application would simply crash. A tighter integration of Python inside AutoCAD - and, I expect, overall better tooling for IronPython when working with .NET classes - would probably help avoid this kind of thrashing, but with what we have today it was pretty painful.

Before we look at the Python code, here’s an update version of the C# PythonLoader functionality: the only real difference being the try-catch block around the code which hosts our IronPython scripting engine and calls ExecuteFile():

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.EditorInput;

using IronPython.Hosting;

using Microsoft.Scripting.Hosting;

using System;

namespace PythonLoader

{

publicclassCommandsAndFunctions

{

[CommandMethod("-PYLOAD")]

publicstaticvoid PythonLoadCmdLine()

{

PythonLoad(true);

}

[CommandMethod("PYLOAD")]

publicstaticvoid PythonLoadUI()

{

PythonLoad(false);

}

publicstaticvoid PythonLoad(bool useCmdLine)

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

short fd =

(short)Application.GetSystemVariable("FILEDIA");

// As the user to select a .py file

PromptOpenFileOptions pfo =

newPromptOpenFileOptions(

"Select Python script to load"

);

pfo.Filter = "Python script (*.py)|*.py";

pfo.PreferCommandLine =

(useCmdLine || fd == 0);

PromptFileNameResult pr =

ed.GetFileNameForOpen(pfo);

// And then try to load and execute it

if (pr.Status == PromptStatus.OK)

ExecutePythonScript(pr.StringResult);

}

[LispFunction("PYLOAD")]

publicResultBuffer PythonLoadLISP(ResultBuffer rb)

{

constint RTSTR = 5005;

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

if (rb == null)

{

ed.WriteMessage("\nError: too few arguments\n");

}

else

{

// We're only really interested in the first argument

Array args = rb.AsArray();

TypedValue tv = (TypedValue)args.GetValue(0);

// Which should be the filename of our script

if (tv != null && tv.TypeCode == RTSTR)

{

// If we manage to execute it, let's return the

// filename as the result of the function

// (just as (arxload) does)

bool success =

ExecutePythonScript(Convert.ToString(tv.Value));

return

(success ?

newResultBuffer(

newTypedValue(RTSTR, tv.Value)

)

: null);

}

}

returnnull;

}

privatestaticbool ExecutePythonScript(string file)

{

// If the file exists, let's load and execute it

// (we could/should probably add some more robust

// exception handling here)

bool ret = System.IO.File.Exists(file);

if (ret)

{

try

{

ScriptEngine engine = Python.CreateEngine();

engine.ExecuteFile(file);

}

catch (System.Exception ex)

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

ed.WriteMessage(

"\nProblem executing script: {0}", ex.Message

);

}

}

return ret;

}

}

}

And here’s the IronPython code for jigging a box inside AutoCAD:

import clr

path = 'C:\\Program Files\\Autodesk\\AutoCAD 2009\\'

clr.AddReferenceToFileAndPath(path + 'acdbmgd.dll')

clr.AddReferenceToFileAndPath(path + 'acmgd.dll')

clr.AddReferenceToFileAndPath(path + 'acmgdinternal.dll')

import Autodesk

import Autodesk.AutoCAD.Runtime as ar

import Autodesk.AutoCAD.ApplicationServices as aas

import Autodesk.AutoCAD.DatabaseServices as ads

import Autodesk.AutoCAD.EditorInput as aei

import Autodesk.AutoCAD.Geometry as ag

import Autodesk.AutoCAD.Internal as ai

from Autodesk.AutoCAD.Internal import Utils

# Function to register AutoCAD commands

# To be used via a function decorator

def autocad_command(function):

# First query the function name

n = function.__name__

# Create the callback and add the command

cc = ai.CommandCallback(function)

Utils.AddCommand('pycmds', n, n, ar.CommandFlags.Modal, cc)

# Let's now write a message to the command-line

doc = aas.Application.DocumentManager.MdiActiveDocument

ed = doc.Editor

ed.WriteMessage("\nRegistered Python command: {0}", n)

# A jig to create a Solid3d - in this case a box

class SolidJig(aei.EntityJig):

# Initialization function

def __init__(self, ent):

# Store the object and call the base class

self._sol = ent

aei.EntityJig.__init__(self, ent)

# The function called to run the jig

def StartJig(self, ed, pt):

# The start point is specific outside the jig

self._start = pt

self._end = pt

return ed.Drag(self)

# The sampler function

def Sampler(self, prompts):

# Set up our selection options

jo = aei.JigPromptPointOptions()

jo.UserInputControls = (

aei.UserInputControls.Accept3dCoordinates |

aei.UserInputControls.NoZeroResponseAccepted |

aei.UserInputControls.NoNegativeResponseAccepted)

jo.Message = "\nSelect end point: "

# Get the end point of our box

res = prompts.AcquirePoint(jo)

if self._end == res.Value:

return aei.SamplerStatus.NoChange

else:

self._end = res.Value

return aei.SamplerStatus.OK

# The update function

def Update(self):

# Recreate our Solid3d box

try:

# Get the width (x) and depth (y)

x = self._end.X - self._start.X

y = self._end.Y - self._start.Y

# We need a non-zero Z value, so we copy Y

z = y

# Create our box and move it to the right place

self._sol.CreateBox(x,y,z)

self._sol.TransformBy(

ag.Matrix3d.Displacement(

ag.Vector3d(

self._start.X + x/2,

self._start.Y + y/2,

self._start.Z + z/2)))

except:

return False

return True

# Create a box using a jig

@autocad_command

def boxjig():

doc = aas.Application.DocumentManager.MdiActiveDocument

db = doc.Database

ed = doc.Editor

# Select the start point before entering the jig

ppr = ed.GetPoint("\nSelect start point: ")

if ppr.Status == aei.PromptStatus.OK:

# We'll add our solid to the modelspace

tr = doc.TransactionManager.StartTransaction()

bt = tr.GetObject(db.BlockTableId, ads.OpenMode.ForRead)

btr = tr.GetObject(

bt[ads.BlockTableRecord.ModelSpace], ads.OpenMode.ForWrite)

# Make sure we're recording history to allow grip editing

sol = ads.Solid3d()

sol.RecordHistory = True

# Now we add our solid

btr.AppendEntity(sol)

tr.AddNewlyCreatedDBObject(sol, True)

# And call the jig before finishing

sj = SolidJig(sol)

ppr2 = sj.StartJig(ed, ppr.Value)

# Only commit if all completed well

if ppr2.Status == aei.PromptStatus.OK:

tr.Commit()

tr.Dispose()

When we execute the PYLOAD command, load our .py file and execute the BOXJIG command, we’ll be able to jig a Solid3d in a non-wireframe 3D view:

Something to note: I haven’t spent time optimizing this code, so there’s a good chance it’s sub-optimal (and may well leak memory). The point was not to demonstrate a definitive approach to solving this particular problem but rather to see whether it was possible to attack a problem of non-trivial complexity with the current toolset and my general lack of knowledge of the Python language.

Well, it’s obviously possible – as I somehow managed to do it – but, to come clean, I did cheat somewhat: I had a C# project open at the same time which I regularly referred to for IntelliSense look-ups. I didn’t do my full development in C# and then convert it to Python, though, so I see this more as a stop-gap to help address some of the current limitations in the tools. That’s the story I’m going with, anyway. :-)

March 20, 2009

I’ve been meaning to play around with the Python language for some time, now, and with the recent release of IronPython 2 it seems a good time to start.

Why Python? A number of people in my team – including Jeremy Tammik and the people within our Media & Entertainment workgroup who support Python’s use with Maya and MotionBuilder – are fierce proponents of the language. I’m told that it’s an extremely easy, general-purpose, dynamic programming language. All of which sounds interesting, of course, although I have to admit I’m less convinced of the importance of the dynamic piece: I’ve found a lot of value in static typing over the years (even F# is statically typed, although many people – even some who work with it - don’t realise this… its type inference system allows you to code safely without specifying types all over the place).

Let’s take a quick step back and talk about what makes a language dynamic. The most common example of a dynamic language – one that I’m sure most of you will have touched at some point – is JavaScript. In JavaScript you declare everything as a var, assign it, call methods on it and hope that they work at runtime. I admit that I’ve always disliked developing in JavaScript because of the lack of decent tool support: I’m a big fan of Intellisense (based on an object’s design-time type) and want the compiler to tell me if I’m dealing with an object that doesn’t support a particular method. But perhaps that’s largely what I’ve become used to from modern development tools, and I’m trying to remain open to new things. Really, I am.

Another dynamic language with which I’ve had much more favourable (but still, at times, frustrating) experiences is LISP. But my relationship with LISP is different: like most early AutoCAD programmers I adopted it out of necessity – and at the time I started with it programming environments were, in any case, generally very basic - I then grew to love it and have since never forgotten it, even when more attractive/productive development environments came along. So I’m extremely loathe to paint it with the same brush as the one I’ve used for JavaScript.

Python is also of interest because of its cross-platform availability: it’s an open source language with its roots in the UNIX/Linux world, but is now gaining popularity across a variety of OS platforms (one of the reasons it’s the scripting language chosen for at least one of our cross-platform products, Autodesk Maya).

So all in all, the world we live in seems to be becoming increasingly dynamic. :-)

Anyway – now on to getting IronPython working with AutoCAD. I had originally hoped to build a .NET assembly directly using IronPython – something that appears to have been enabled with the 2.0 release of IronPython - which could then be loaded into AutoCAD. Unfortunately this was an exercise in frustration: AutoCAD makes heavy use of custom attributes for identifying commands etc., but IronPython doesn’t currently support the use of attributes. It is possible to do some clever stuff by compiling attributed C# on-the-fly and deriving classes from it (information on this is available here), which will – in theory, at least – get you something in memory that’s attributed but, as AutoCAD scans the physical assembly for custom attributes before loading it, this didn’t help. I also spent a great deal of time just trying to derive a class from Autodesk.AutoCAD.Runtime.IExtensionApplication – to have the Initialize() function called automatically on load – but I just couldn’t get this to work, either.

Then, thankfully, Tim Riley came to the rescue: we’ve been in touch on and off over the years since he started the PyAcad.NET project to run IronPython code inside AutoCAD, and Tim was able to put together some working code which actually registered commands (after I’d pointed him at a function he could use from AutoCAD 2009’s acmgdinternal.dll – an unsupported assembly that exposes some otherwise quite helpful functions). He ended up choosing an implementation that had also been suggested to me by Albert Szilvasy: to implement a PYLOAD command using C# which allows selection and loading of a Python script (because Python is, ultimately, all about scripting rather than building static, compiled assemblies).

Before we get on to the C# module, I should point out that I installed IronPython 2.0.1 as well as IronPython Studio 1.0 for the Visual Studio 2008 integration. It turns out that as we’re relying on C# to manage the loading of Python – rather than compiling a .NET assembly – the main advantage of IronPython Studio is around the ability to work with Python source code inside Visual Studio.

To build the below C# code into a standard .NET Class Library assembly (a .DLL) you’ll need to add assembly references to IronPython.dll, IronPython.Modules.dll, Microsoft.Scripting.dll and Microsoft.Scripting.Core.dll – all of which can be found in the main IronPython install folder (on my system this is in “C:\Program Files\IronPython 2.0.1”). As well as the standard references to acmgd.dll and acdbmgd.dll, of course.

Here’s the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.EditorInput;

using IronPython.Hosting;

using Microsoft.Scripting.Hosting;

using System;

namespace PythonLoader

{

publicclassCommandsAndFunctions

{

[CommandMethod("-PYLOAD")]

publicstaticvoid PythonLoadCmdLine()

{

PythonLoad(true);

}

[CommandMethod("PYLOAD")]

publicstaticvoid PythonLoadUI()

{

PythonLoad(false);

}

publicstaticvoid PythonLoad(bool useCmdLine)

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

short fd =

(short)Application.GetSystemVariable("FILEDIA");

// As the user to select a .py file

PromptOpenFileOptions pfo =

newPromptOpenFileOptions(

"Select Python script to load"

);

pfo.Filter = "Python script (*.py)|*.py";

pfo.PreferCommandLine =

(useCmdLine || fd == 0);

PromptFileNameResult pr =

ed.GetFileNameForOpen(pfo);

// And then try to load and execute it

if (pr.Status == PromptStatus.OK)

ExecutePythonScript(pr.StringResult);

}

[LispFunction("PYLOAD")]

publicResultBuffer PythonLoadLISP(ResultBuffer rb)

{

constint RTSTR = 5005;

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

if (rb == null)

{

ed.WriteMessage("\nError: too few arguments\n");

}

else

{

// We're only really interested in the first argument

Array args = rb.AsArray();

TypedValue tv = (TypedValue)args.GetValue(0);

// Which should be the filename of our script

if (tv != null && tv.TypeCode == RTSTR)

{

// If we manage to execute it, let's return the

// filename as the result of the function

// (just as (arxload) does)

bool success =

ExecutePythonScript(Convert.ToString(tv.Value));

return

(success ?

newResultBuffer(

newTypedValue(RTSTR, tv.Value)

)

: null);

}

}

returnnull;

}

privatestaticbool ExecutePythonScript(string file)

{

// If the file exists, let's load and execute it

// (we could/should probably add some more robust

// exception handling here)

bool ret = System.IO.File.Exists(file);

if (ret)

{

ScriptEngine engine = Python.CreateEngine();

engine.ExecuteFile(file);

}

return ret;

}

}

}

The code behind the PYLOAD command is actually really simple. I could have kept it basic but decided it would be a good opportunity to show some best practices. So not only do we have the standard PYLOAD command, which respects the FILEDIA variable to decide whether to use dialogs or the command-line, we also have a command-line version –PYLOAD and a LISP function (pyload). All of which call into the same function to load a Python script.

OK, now let’s take a look at a simple IronPython script that calls into AutoCAD via its .NET API. Thanks again to Tim Riley for providing something that works. Even with Python being (apparently) so easy to learn, I’m such a neophyte that without his help I’d still be stumbling around in the dark.

import clr

path = 'C:\\Program Files\\Autodesk\\AutoCAD 2009\\'

clr.AddReferenceToFileAndPath(path + 'acdbmgd.dll')

clr.AddReferenceToFileAndPath(path + 'acmgd.dll')

clr.AddReferenceToFileAndPath(path + 'acmgdinternal.dll')

import Autodesk

import Autodesk.AutoCAD.Runtime as ar

import Autodesk.AutoCAD.ApplicationServices as aas

import Autodesk.AutoCAD.DatabaseServices as ads

import Autodesk.AutoCAD.Geometry as ag

import Autodesk.AutoCAD.Internal as ai

from Autodesk.AutoCAD.Internal import Utils

# Function to register AutoCAD commands

# To be used via a function decorator

def autocad_command(function):

# First query the function name

n = function.__name__

# Create the callback and add the command

cc = ai.CommandCallback(function)

Utils.AddCommand('pycmds', n, n, ar.CommandFlags.Modal, cc)

# Let's now write a message to the command-line

doc = aas.Application.DocumentManager.MdiActiveDocument

ed = doc.Editor

ed.WriteMessage("\nRegistered Python command: {0}", n)

# A simple "Hello World!" command

@autocad_command

def msg():

doc = aas.Application.DocumentManager.MdiActiveDocument

ed = doc.Editor

ed.WriteMessage("\nOur test command works!")

# And one to do something a little more complex...

# Adds a circle to the current space

@autocad_command

def mycir():

doc = aas.Application.DocumentManager.MdiActiveDocument

db = doc.Database

tr = doc.TransactionManager.StartTransaction()

bt = tr.GetObject(db.BlockTableId, ads.OpenMode.ForRead)

btr = tr.GetObject(db.CurrentSpaceId, ads.OpenMode.ForWrite)

cir = ads.Circle(ag.Point3d(10,10,0),ag.Vector3d.ZAxis, 2)

btr.AppendEntity(cir)

tr.AddNewlyCreatedDBObject(cir, True)

tr.Commit()

tr.Dispose()

As we’re stuck without the ability to use custom attributes in IronPython, we’re making use of the Autodesk.AutoCAD.Internal namespace to register commands at runtime. I don’t like doing this, but at the same time I was left with little choice, unless we choose to find another way to call into the code. Please be warned that anything contained in the Autodesk.AutoCAD.Internal namespace is unsupported functionality, and subject to change without warning.

Now that I have that off my chest, let’s comment a little further on the above code…

Even without custom attributes, we have used a pretty cool Python language feature known as decorators (thanks *again* for the tip, Tim :-) which helps us to mark functions as commands. The autocad_command function is called for each decorated function, and this is where we register a command for the function based on the function’s name. Pretty cool.

You’ll notice a distinct lack of types in the code (and yes, that still scares me). When I was previously trying to compile a DLL based on this code, I had a lot of trouble getting anything at all to fail at compile-time, but clearly a lot would fail at runtime (when I could actually get anything to execute :-S). I feel as though I still need to get my head around this trade-off: I can see the argument for simplicity/elegance/succinctness – and even the power it brings in some situations - but the Computer Scientist in me is screaming for safety/reliability/determinism/debuggability (if that’s even a word). Oh well. The main thing is that I’m starting the journey, at least: we’ll see if it ends up somewhere I like. :-)

When we build and NETLOAD our PythonLoader C# application and execute the PYLOAD command, we can select our Python script:

Command: PYLOAD

Once selected, the script gets loaded and should register a couple of commands:

Registered Python command: msg

Registered Python command: mycir

Running the MSG command will execute a simple “Hello World!”-like function, just printing a message to the command-line:

Command: MSG

Our test command works!

And running the MYCIR command should just add a simple circle to the current space in the active drawing.

Command: MYCIR

That’s it for my initial foray into the world of Python. I hope you’ve found this helpful and enjoy playing around with the Python programming language inside AutoCAD. Please do post a comment if you have experiences or anecdotes to share on this topic!

March 18, 2009

Thanks, once again, to Philippe Leefsma, a DevTech engineer based in Prague, for contributing the code for this post. While researching an issue he was working on Philippe stumbled across a comment on this previous post where I more-or-less said jigging attributes wasn’t possible. Ahem. Anyway, Philippe decided to – quite rightly – prove me wrong, and the result is today’s post. :-)

It turns out that the trick to jigging a block with attributes is to add the block reference to the database prior to running the jig. I’d been coming at this from another direction – working out how to call through to the right version of the ObjectARX function, the one that allows the block reference to be in-memory rather than db-resident – but Philippe’s approach means that’s no longer needed. I see this technique as potentially being useful when jigging other entities that benefit from being database resident (Solid3d objects spring to mind), so I really appreciate Philippe’s hard work on this.

Here’s the C# code which I’ve edited for posting:

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using System.Collections.Generic;

namespace BlockJig

{

classCBlockJig : EntityJig

{

privatePoint3d _pos;

privateDictionary<string, Point3d> _attPos;

privateTransaction _tr;

public CBlockJig(Transaction tr, BlockReference br)

: base(br)

{

_pos = br.Position;

// Initialize our dictionary with the tag /

// AttributeDefinition position

_attPos = newDictionary<string, Point3d>();

_tr = tr;

BlockTableRecord btr =

(BlockTableRecord)_tr.GetObject(

br.BlockTableRecord,

OpenMode.ForRead

);

if (btr.HasAttributeDefinitions)

{

foreach (ObjectId id in btr)

{

DBObject obj =

tr.GetObject(id, OpenMode.ForRead);

AttributeDefinition ad =

obj asAttributeDefinition;

if (ad != null)

{

_attPos.Add(ad.Tag, ad.Position);

}

}

}

}

protectedoverridebool Update()

{

BlockReference br = Entity asBlockReference;

br.Position = _pos;

if (br.AttributeCollection.Count != 0)

{

foreach (ObjectId id in br.AttributeCollection)

{

DBObject obj =

_tr.GetObject(id, OpenMode.ForRead);

AttributeReference ar =

obj asAttributeReference;

// Apply block transform to att def position

if (ar != null)

{

ar.UpgradeOpen();

ar.Position =

_attPos[ar.Tag].TransformBy(br.BlockTransform);

}

}

}

returntrue;

}

protectedoverrideSamplerStatus Sampler(JigPrompts prompts)

{

JigPromptPointOptions opts =

newJigPromptPointOptions("\nSelect insertion point:");

opts.BasePoint = newPoint3d(0, 0, 0);

opts.UserInputControls =

UserInputControls.NoZeroResponseAccepted;

PromptPointResult ppr = prompts.AcquirePoint(opts);

if (_pos == ppr.Value)

{

returnSamplerStatus.NoChange;

}

_pos = ppr.Value;

returnSamplerStatus.OK;

}

publicPromptStatus Run()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

PromptResult promptResult = ed.Drag(this);

return promptResult.Status;

}

}

publicclassCommands

{

[CommandMethod("BJ")]

staticpublicvoid BlockJig()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Database db = doc.Database;

Editor ed = doc.Editor;

PromptStringOptions pso =

newPromptStringOptions("\nEnter block name: ");

PromptResult pr = ed.GetString(pso);

if (pr.Status != PromptStatus.OK)

return;

Transaction tr =

doc.TransactionManager.StartTransaction();

using (tr)

{

BlockTable bt =

(BlockTable)tr.GetObject(

db.BlockTableId,

OpenMode.ForRead

);

BlockTableRecord space =

(BlockTableRecord)tr.GetObject(

db.CurrentSpaceId,

OpenMode.ForRead

);

if (!bt.Has(pr.StringResult))

{

ed.WriteMessage(

"\nBlock \"" + pr.StringResult + "\" not found.");

return;

}

space.UpgradeOpen();

BlockTableRecord btr =

(BlockTableRecord)tr.GetObject(

bt[pr.StringResult],

OpenMode.ForRead);

// Block needs to be inserted to current space before

// being able to append attribute to it

BlockReference br =

newBlockReference(newPoint3d(), btr.ObjectId);

space.AppendEntity(br);

tr.AddNewlyCreatedDBObject(br, true);

if (btr.HasAttributeDefinitions)

{

foreach (ObjectId id in btr)

{

DBObject obj =

tr.GetObject(id, OpenMode.ForRead);

AttributeDefinition ad =

obj asAttributeDefinition;

if (ad != null && !ad.Constant)

{

AttributeReference ar =

newAttributeReference();

ar.SetAttributeFromBlock(ad, br.BlockTransform);

ar.Position =

ad.Position.TransformBy(br.BlockTransform);

ar.TextString = ad.TextString;

br.AttributeCollection.AppendAttribute(ar);

tr.AddNewlyCreatedDBObject(ar, true);

}

}

}

// Run the jig

CBlockJig myJig = newCBlockJig(tr, br);

if (myJig.Run() != PromptStatus.OK)

return;

// Commit changes if user accepted, otherwise discard

tr.Commit();

}

}

}

}

When you run the BJ command (short for BlockJig) and specify the name of a block in the current drawing which contains attributes, you’ll now see the attributes with their default values shown as part of the block being jigged. Implementing the code to allow editing of those attributes after insertion is left as an exercise for the reader.

Update:

This code didn't work for a few situations, such as when using justification (attributes would end up at the origin after being dragged) or with MText attributes (which would start at the origin until the mouse was moved).

A big thanks to Roland Feletic from PAUSER ZT-GMBH for helping identify and diagnose the various cases.

Here's the updated C# code:

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using System.Collections.Generic;

namespace BlockJigApplication

{

classAttInfo

{

privatePoint3d _pos;

privatePoint3d _aln;

privatebool _aligned;

public AttInfo(Point3d pos, Point3d aln, bool aligned)

{

_pos = pos;

_aln = aln;

_aligned = aligned;

}

publicPoint3d Position

{

set { _pos = value; }

get { return _pos; }

}

publicPoint3d Alignment

{

set { _aln = value; }

get { return _aln; }

}

publicbool IsAligned

{

set { _aligned = value; }

get { return _aligned; }

}

}

classBlockJig : EntityJig

{

privatePoint3d _pos;

privateDictionary<ObjectId, AttInfo> _attInfo;

privateTransaction _tr;

public BlockJig(

Transaction tr,

BlockReference br,

Dictionary<ObjectId, AttInfo> attInfo

) : base(br)

{

_pos = br.Position;

_attInfo = attInfo;

_tr = tr;

}

protectedoverridebool Update()

{

BlockReference br = Entity asBlockReference;

br.Position = _pos;

if (br.AttributeCollection.Count != 0)

{

foreach (ObjectId id in br.AttributeCollection)

{

DBObject obj =

_tr.GetObject(id, OpenMode.ForRead);

AttributeReference ar =

obj asAttributeReference;

// Apply block transform to att def position

if (ar != null)

{

ar.UpgradeOpen();

AttInfo ai = _attInfo[ar.ObjectId];

ar.Position =

ai.Position.TransformBy(br.BlockTransform);

if (ai.IsAligned)

{

ar.AlignmentPoint =

ai.Alignment.TransformBy(br.BlockTransform);

}

if (ar.IsMTextAttribute)

{

ar.UpdateMTextAttribute();

}

}

}

}

returntrue;

}

protectedoverrideSamplerStatus Sampler(JigPrompts prompts)

{

JigPromptPointOptions opts =

newJigPromptPointOptions("\nSelect insertion point:");

opts.BasePoint = newPoint3d(0, 0, 0);

opts.UserInputControls =

UserInputControls.NoZeroResponseAccepted;

PromptPointResult ppr = prompts.AcquirePoint(opts);

if (_pos == ppr.Value)

{

returnSamplerStatus.NoChange;

}

_pos = ppr.Value;

returnSamplerStatus.OK;

}

publicPromptStatus Run()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Editor ed = doc.Editor;

PromptResult promptResult = ed.Drag(this);

return promptResult.Status;

}

}

publicclassCommands

{

[CommandMethod("BJ")]

staticpublicvoid BlockJigCmd()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Database db = doc.Database;

Editor ed = doc.Editor;

PromptStringOptions pso =

newPromptStringOptions("\nEnter block name: ");

PromptResult pr = ed.GetString(pso);

if (pr.Status != PromptStatus.OK)

return;

Transaction tr =

doc.TransactionManager.StartTransaction();

using (tr)

{

BlockTable bt =

(BlockTable)tr.GetObject(

db.BlockTableId,

OpenMode.ForRead

);

if (!bt.Has(pr.StringResult))

{

ed.WriteMessage(

"\nBlock \"" + pr.StringResult + "\" not found.");

return;

}

BlockTableRecord space =

(BlockTableRecord)tr.GetObject(

db.CurrentSpaceId,

OpenMode.ForWrite

);

BlockTableRecord btr =

(BlockTableRecord)tr.GetObject(

bt[pr.StringResult],

OpenMode.ForRead);

// Block needs to be inserted to current space before

// being able to append attribute to it

BlockReference br =

newBlockReference(newPoint3d(), btr.ObjectId);

space.AppendEntity(br);

tr.AddNewlyCreatedDBObject(br, true);

Dictionary<ObjectId, AttInfo> attInfo =

newDictionary<ObjectId,AttInfo>();

if (btr.HasAttributeDefinitions)

{

foreach (ObjectId id in btr)

{

DBObject obj =

tr.GetObject(id, OpenMode.ForRead);

AttributeDefinition ad =

obj asAttributeDefinition;

if (ad != null && !ad.Constant)

{

AttributeReference ar =

newAttributeReference();

ar.SetAttributeFromBlock(ad, br.BlockTransform);

ar.Position =

ad.Position.TransformBy(br.BlockTransform);

if (ad.Justify != AttachmentPoint.BaseLeft)

{

ar.AlignmentPoint =

ad.AlignmentPoint.TransformBy(br.BlockTransform);

}

if (ar.IsMTextAttribute)

{

ar.UpdateMTextAttribute();

}

ar.TextString = ad.TextString;

ObjectId arId =

br.AttributeCollection.AppendAttribute(ar);

tr.AddNewlyCreatedDBObject(ar, true);

// Initialize our dictionary with the ObjectId of

// the attribute reference + attribute definition info

attInfo.Add(

arId,

newAttInfo(

ad.Position,

ad.AlignmentPoint,

ad.Justify != AttachmentPoint.BaseLeft

)

);

}

}

}

// Run the jig

BlockJig myJig = newBlockJig(tr, br, attInfo);

if (myJig.Run() != PromptStatus.OK)

return;

// Commit changes if user accepted, otherwise discard

tr.Commit();

}

}

}

}

A few comments on this code:

It's been refactored to make a single pass through the block definition to both create the block reference and collect the attribute information to store in our dictionary.

The attribute information is now held in a class, which allows us to store more than just the position in our dictionary (without using multiple dictionaries), I went to the effort of exposing public properties for the various private members, as this is generally a good technique to use (if a little redundant, here).

The dictionary now stores this attribute information against an ObjectId rather than the tag string. Roland made the excellent point that blocks can contain attributes with duplicate tags, so this is much safe. We also had to use the ObjectId of the AttributeReference, as later on inside the jig's Update() function we no longer have access to the AttributeDefinition.

March 16, 2009

In the last post we looked at the code behind an ObjectARX module exposing AutoCAD's Properties Palette for use from managed .NET languages. Thanks again to Cyrille Fauvel for providing this implementation. In this post we're going to move right onto using this implementation from C#.

First things first: if you didn't understand much of what was said in the previous post in this series, Don't Panic! (Yes, that's a quick reference to The Hitchhiker's Guide to the Galaxy.) The actual implementation details aren't particularly important - you only really need to understand them if you want to expose additional interfaces to .NET in the same way (such as IFilterableProperty, the interface discussed in this previous post and something I expect to extend the application to handle, at some point).

Here is the project that contains both the ObjectARX module implementing exposing these interfaces and the sample we're showing today. You simply have to make sure the .NET module can find our asdkOPMNetExt.dll module (ideally both this and your .NET module should be located under AutoCAD's program folder).

Then you can simply implement code, as in the below sample, which loads and makes use of interfaces exposed by this assembly.

Here’s the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Windows.OPM;

using System;

using System.Reflection;

using System.Runtime.InteropServices;

namespace OPMNetSample

{

#region Our Custom Property

[

Guid("F60AE3DA-0373-4d24-82D2-B2646517ABCB"),

ProgId("OPMNetSample.CustomProperty.1"),

// No class interface is generated for this class and

// no interface is marked as the default.

// Users are expected to expose functionality through

// interfaces that will be explicitly exposed by the object

// This means the object can only expose interfaces we define

ClassInterface(ClassInterfaceType.None),

// Set the default COM interface that will be used for

// Automation. Languages like: C#, C++ and VB allow to

//query for interface's we're interested in but Automation

// only aware languages like javascript do not allow to

// query interface(s) and create only the default one

ComDefaultInterface(typeof(IDynamicProperty2)),

ComVisible(true)

]

publicclassCustomProp : IDynamicProperty2

{

privateIDynamicPropertyNotify2 m_pSink = null;

// Unique property ID

publicvoid GetGUID(outGuid propGUID)

{

propGUID =

newGuid("F60AE3DA-0373-4d24-82D2-B2646517ABCB");

}

// Property display name

publicvoid GetDisplayName(outstring szName)

{

szName = "My integer property";

}

// Show/Hide property in the OPM, for this object instance

publicvoid IsPropertyEnabled(object pUnk, outint bEnabled)

{

bEnabled = 1;

}

// Is property showing but disabled

publicvoid IsPropertyReadOnly(outint bReadonly)

{

bReadonly = 0;

}

// Get the property description string

publicvoid GetDescription(outstring szName)

{

szName =

"This property is an integer";

}

// OPM will typically display these in an edit field

// optional: meta data representing property type name,

// ex. ACAD_ANGLE

publicvoid GetCurrentValueName(outstring szName)

{

thrownew System.NotImplementedException();

}

// What is the property type, ex. VT_R8

publicvoid GetCurrentValueType(outushort varType)

{

// The Property Inspector supports the following data

// types for dynamic properties:

// VT_I2, VT_I4, VT_R4, VT_R8,VT_BSTR, VT_BOOL

// and VT_USERDEFINED.

varType = 3; // VT_I4

}

// Get the property value, passes the specific object

// we need the property value for.

publicvoid GetCurrentValueData(object pUnk, refobject pVarData)

{

// TODO: Get the value and return it to AutoCAD

// Because we said the value type was a 32b int (VT_I4)

pVarData = (int)4;

}

// Set the property value, passes the specific object we

// want to set the property value for

publicvoid SetCurrentValueData(object pUnk, object varData)

{

// TODO: Save the value returned to you

// Because we said the value type was a 32b int (VT_I4)

int myVal = (int)varData;

}

// OPM passes its implementation of IDynamicPropertyNotify, you

// cache it and call it to inform OPM your property has changed

publicvoid Connect(object pSink)

{

m_pSink = (IDynamicPropertyNotify2)pSink;

}

publicvoid Disconnect() {

m_pSink = null;

}

}

#endregion

#region Application Entry Point

publicclassMyEntryPoint : IExtensionApplication

{

protected internalCustomProp custProp = null;

publicvoid Initialize()

{

Assembly.LoadFrom("asdkOPMNetExt.dll");

// Add the Dynamic Property

Dictionary classDict = SystemObjects.ClassDictionary;

RXClass lineDesc = (RXClass)classDict.At("AcDbLine");

IPropertyManager2 pPropMan =

(IPropertyManager2)xOPM.xGET_OPMPROPERTY_MANAGER(lineDesc);

custProp = newCustomProp();

pPropMan.AddProperty((object)custProp);

}

publicvoid Terminate()

{

// Remove the Dynamic Property

Dictionary classDict = SystemObjects.ClassDictionary;

RXClass lineDesc = (RXClass)classDict.At("AcDbLine");

IPropertyManager2 pPropMan =

(IPropertyManager2)xOPM.xGET_OPMPROPERTY_MANAGER(lineDesc);

pPropMan.RemoveProperty((object)custProp);

custProp = null;

}

}

#endregion

}

A few comments on what this code does…

It defines a class for our custom dynamic property (CustomProp), for which we need a unique GUID (and yes, by definition GUIDs are unique, but if you use the same one twice it ceases to be :-), and implement various callbacks to indicate the name, type, description and writeability of the property, as well as methods to get and set the property value. For this example our property is called “My integer property”, and – guess what? – it’s an integer, and has been hardcoded to have the value 4. We’re not actually storing the data being exposed via this property, but in a real-world application you would probably store it either as XData attached to the object, inside an XRecord in the object’s extension dictionary or in an external database of some kind.

In the rest of the code we’re defining functions that are called when the module is loaded and when AutoCAD terminates (see this previous post for more information on this mechanism). We use the Initialize() callback to load our mixed-mode module for which we presented the code in the last post (asdkOPMNetExt.dll) and then go ahead and instantiate our property and attach it to line objects.

When we build and load the sample, making sure the Properties Palette is visible (using the PROPS command or by double-clicking on the line) and selecting a line we’ve just drawn, we should see our dynamic property appear, as shown in this image.

If you don’t see the property appear, the application is probably having trouble loading asdkOPMNetExt.dll: as mentioned earlier I have placed both this and the sample application in the root folder of my AutoCAD installation. If you’re not sure whether the module is loaded properly, you can step through the Initialize() function in the debugger or add a simple command to your code which will obviously only work if your application has been loaded (the Assembly.LoadFrom() call will throw an exception if it doesn’t find the module, and if an exception is thrown from Initialize() the application will not be loaded, as described in this previous post).

For the sake of simplicity I’m going to leave this basic sample as having just one, hardwired property: hopefully it’s obvious how the application could be extended to handle more properties and to store these properties with their objects (if not, let me know by posting a comment and I’ll put together a more extensive example when I get the chance).

March 13, 2009

A huge thanks to Cyrille Fauvel, who manages DevTech's Media & Entertainment team but still finds the time to dip into the odd deeply-technical, AutoCAD-related issue. Cyrille provided the original article on this topic late last year, but it's taken me time to get around to editing and publishing it. A quick tip... if you're not interested in the technical details of how Cyrille has exposed the various Properties Palette interfaces to .NET, you can safely skip this post and join us again when we go ahead and make use of the implementation to add dynamic properties to core AutoCAD objects using C#. Most people won't want to know all the details included in this post, but are just interested in the end results: next time I'll be providing a pre-built ObjectARX module, along with full source-code, that can be loaded into AutoCAD 2007-2009 to enable the use of the Properties Palette from .NET.

AutoCAD's Properties Palette - once known as the Object Properties Manager (OPM) - is a very handy way to display properties inside your application, whether those properties are associated with individual objects or with the application itself. The Properties Palette uses COM to communicate with the object(s) in question, and has always required the use of C++ to expose particular interfaces that control the display of the properties in the palette, so its functionality has not been available to developers using managed .NET languages such as C# and VB.NET.

There is some portion of the Properties Palette functionality exposed via the Autodesk.AutoCAD.Windows.ToolPalette namespace, such as the IAcPiPropertyDisplay interface allowing objects and commands to customize the display of properties in the property inspector window, but this is far from complete. This post looks at exposing more of the standard Properties Palette functionality to .NET languages, and next time we'll look at some specific examples of using it from C#.

Our first step is to expose some "OPM" interfaces to .NET. There are two ways to do this: we can expose them from our .NET application sung standard C# or VB.NET code, or we can use a small ObjectARX module to implement them.

Well, we have to use ObjectARX to access some OPM functionality that is currently only exposed via C++, so we're going to go down that path. What we're going to do is expose the interface(s) we want from our ObjectARX module and marshal the various parameters to be useable from .NET. Simple! :-)

To get started, we're going to look at AutoCAD's IPropertyManager2 interface, which is contained in dynprops.h on the ObjectARX SDK:

Now in our ObjectARX application we're going to expose the IPropertyManager2 interface to .NET.

At line 41 in the IPropertyManager2.h file in the provided project [which will actually be provided with the final post of the series], we declare the interface using the same Global Unique Identifier (GUID):

[InteropServices::Guid("FABC1C70-1044-4aa0-BF8D-91FFF9052715")]

Then we need to indicate that our interface is derived from the standard, root COM interface, IUnknown:

Now for the interface itself. We declare that it's a "public interface class" along with its various members, specifying how to marshal all the types. We're not going to return values from our methods (which usually indicate success or failure): we'll leave it to the .NET code that implements the interfaces (which we'll see in the final post of the series) to throw exceptions instead.

Here's the full class declaration, edited for display on this blog:

namespace Autodesk

{

namespace AutoCAD

{

namespace Windows

{

namespace OPM

{

[InteropServices::Guid(

"FABC1C70-1044-4aa0-BF8D-91FFF9052715"

)]

[InteropServices::InterfaceTypeAttribute(

InteropServices::ComInterfaceType::InterfaceIsIUnknown

)]

[InteropServices::ComVisible(true)]

publicinterfaceclass IPropertyManager2

{

void AddProperty(

[InteropServices::In,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::IUnknown

)

] Object^ pDynPropObj

);

void RemoveProperty(

[InteropServices::In,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::IUnknown

)

] Object^ pDynPropObj

);

void GetDynamicProperty(

[InteropServices::In] long index,

[InteropServices::Out,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::IUnknown

)

] interior_ptr<Object^> value

);

void GetDynamicPropertyByName(

[InteropServices::In,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::BStr

)

] System::String^ name,

[InteropServices::Out,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::IUnknown

)

] interior_ptr<Object^> value

);

void GetDynamicPropertyCountEx(

[InteropServices::Out] long* count

);

void GetDynamicClassInfo(

[InteropServices::In,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::IUnknown

)

] Object^ pDynPropObj,

[InteropServices::Out,

InteropServices::MarshalAs(

/*InteropServices::UnmanagedType::ITypeInfo*/

InteropServices::UnmanagedType::IUnknown

)

] interior_ptr<Object^> typeInfo,

[InteropServices::Out] ulong* dwCookie

);

};

}

}

}

}

This one class is really at the core of our effort to expose the OPM to .NET. To let us access and use this class, we're going to expose a "property extension factory". We might have opted to P/Invoke an ObjectARX API for this, but it's ultimately cleaner to implement this from C++ and expose it via a managed wrapper.

So how do we know we need a property extension factory? Typically to access the IPropertyManager2 instance from ObjectARX we use the GET_OPMPROPERTY_MANAGER() macro. From dynprops.h, we know that this macro expands into GET_OPMEXTENSION_CREATE_PROTOCOL()->CreateOPMObjectProtocol(pAcRxClass)->GetPropertyManager() and the GET_OPMEXTENSION_CREATE_PROTOCOL() macro expands, in turn, into OPMPropertyExtensionFactory::cast(AcDbDatabase::desc()->queryX(OPMPropertyExtensionFactory::desc())). This is how we know we need to expose the OPMPropertyExtensionFactory class.

So let's take a look at the declaration in the ObjectARX SDK of OPMPropertyExtensionFactory, once again from dynprops.h:

//--------------------------

// OPMPropertyExtension interface

// This class is implemented by AutoCAD and available through

// GET_OPMEXTENSION_CREATE_PROTOCOL. You can add property classes

// by calling GET_OPMPROPERTY_MANAGER for a particular AcRxClass

// to get the property manager for that class.

// You can also enumerate the dynamic properties which have

// been added to that class as well as its base class(es) via

// GetPropertyCount and GetPropertyClassArray

//--------------------------

class OPMPropertyExtensionFactory: public AcRxObject

{

public:

ACRX_DECLARE_MEMBERS(OPMPropertyExtensionFactory);

virtual ~OPMPropertyExtensionFactory(){}

//Retrieves the OPMPropertyExtension for the specified class, if the

//extension has not been added before, it creates it. Note: the implementation

//of this class manages the lifetime of OPMPropertyExtension, as such you don't

To expose this class through .NET we're going to use the Autodesk::AutoCAD::Runtime::Wrapper attribute class to tell AutoCAD to manage this class as an internal class object wrapper. We then derive our wrapper class from RXObject (as the base class of the corresponding ObjectARX class is AcRxObject), and implement a constructor, a GetImpObj() method to provide access to the unmanaged object and our factory function itself, CreateOPMObjectProtocol(). There's no need for a destructor as this is handled by the wrapper.

Here's the complete class declaration from OPMPropertyExtensionFactory.h in the provided sample:

namespace Autodesk

{

namespace AutoCAD

{

namespace Windows

{

namespace OPM

{

[Autodesk::AutoCAD::Runtime::Wrapper(

"OPMPropertyExtensionFactory"

)]

publicrefclass AcMgdOPMPropertyExtensionFactory

: public RXObject

{

publicprotected:

AcMgdOPMPropertyExtensionFactory(

System::IntPtr unmanagedPointer,

bool bAutoDelete

)

: RXObject(unmanagedPointer, bAutoDelete) {}

internal:

//- Returns the unmanaged ARX Object

inline OPMPropertyExtensionFactory* GetImpObj()

{

return(

static_cast<OPMPropertyExtensionFactory *>(

UnmanagedObject.ToPointer()

)

);

}

public:

virtual AcMgdOPMPropertyExtension^ CreateOPMObjectProtocol(

RXClass^ runtimeClass,

long lReserved

);

} ;

}

}

}

}

There is one last class that needs to be exposed: OPMPropertyExtension, which is what is created by the OPMPropertyExtensionFactory (logically enough). We'll use the same technique as for the OPMPropertyExtensionFactory.

Here's the class declaration from OPMPropertyExtension.h:

namespace Autodesk

{

namespace AutoCAD

{

namespace Windows

{

namespace OPM

{

[Autodesk::AutoCAD::Runtime::Wrapper(

"OPMPropertyExtension"

)]

publicrefclass AcMgdOPMPropertyExtension : public RXObject

{

publicprotected:

AcMgdOPMPropertyExtension(

System::IntPtr unmanagedPointer,

bool bAutoDelete

)

: RXObject (unmanagedPointer, bAutoDelete) {}

internal:

//- Returns the unmanaged ARX Object

inline OPMPropertyExtension* GetImpObj()

{

return (

static_cast<OPMPropertyExtension *>(

UnmanagedObject.ToPointer()

)

);

}

public:

virtual Object^ GetPropertyManager();

virtualvoid SetPropertyManager(Object^ pPropManager);

} ;

}

}

}

}

Those are all the declarations (yes, in C++ you need declare your classes separately from their definitions... yawn... :-) so let's go ahead and implement the wrapper classes.

First the OPMPropertyExtensionFactory class (from OPMPropertyExtensionFactory.cpp), with it's main method to create and return a new OPMPropertyExtension:

namespace Autodesk

{

namespace AutoCAD

{

namespace Windows

{

namespace OPM

{

AcMgdOPMPropertyExtension^

AcMgdOPMPropertyExtensionFactory::CreateOPMObjectProtocol(

RXClass^ runtimeClass, long lReserved

)

{

return (

gcnew AcMgdOPMPropertyExtension(

System::IntPtr(

GetImpObj()->CreateOPMObjectProtocol(

static_cast<AcRxClass*>(

//runtimeClass->GetImpObj()

runtimeClass->UnmanagedObject.ToPointer()

),

lReserved

)

),

false

)

);

}

}

}

}

}

Here's the implementation of the OPMPropertyExtension class (from OPMPropertyExtension.cpp), which gets us access to the OPMPropertyManager:

namespace Autodesk

{

namespace AutoCAD

{

namespace Windows

{

namespace OPM

{

Object^ AcMgdOPMPropertyExtension::GetPropertyManager()

{

IUnknown *pUnk =

GetImpObj()->GetPropertyManager();

return (

System::Runtime::InteropServices::Marshal::

GetObjectForIUnknown(System::IntPtr(pUnk))

);

}

void AcMgdOPMPropertyExtension::SetPropertyManager(

Object^ pPropManager

)

{

IPropertyManager *pPropMgr =

reinterpret_cast<IPropertyManager *>(

System::Runtime::InteropServices::Marshal::

GetIUnknownForObject(pPropManager).ToPointer()

);

GetImpObj()->SetPropertyManager(pPropMgr);

}

}

}

}

}

To finish off our implementation, we're going to re-implement the two handy macros we have in unmanaged C++ (i.e. ObjectARX), to give us the same capabilities when in a managed environment.

Here's our implementation from xOPM.cpp:

namespace Autodesk

{

namespace AutoCAD

{

namespace Windows

{

namespace OPM

{

AcMgdOPMPropertyExtensionFactory^

xOPM::xGET_OPMEXTENSION_CREATE_PROTOCOL()

{

Dictionary^ classDict =

SystemObjects::ClassDictionary;

RXClass^ opmFactoryClass =

(RXClass^)classDict->At("OPMPropertyExtensionFactory");

RXClass^ dbClass =

(RXClass^)classDict->At("AcDbDatabase");

return(

gcnew AcMgdOPMPropertyExtensionFactory(

dbClass->QueryX (opmFactoryClass),

false

)

);

}

Object^

xOPM::xGET_OPMPROPERTY_MANAGER(RXClass^ pAcRxClass)

{

AcMgdOPMPropertyExtensionFactory^ opmFactory =

xOPM::xGET_OPMEXTENSION_CREATE_PROTOCOL();

return(

opmFactory->

CreateOPMObjectProtocol(pAcRxClass, 0)->

GetPropertyManager());

}

}

}

}

}

There's just one more interface that needs exposing to .NET, and that's IDynamicProperty2. We're going to expose this in much the same way as we did IPropertyManager2.

Here's the original declaration from dynprops.h:

//--------------------------

// IDynamicProperty2 interface

// Implement this class to create dynamic properties for the PropertyPalette

And here's our own exposure of this interface to .NET (from IDynamicProperty2.h):

namespace Autodesk

{

namespace AutoCAD

{

namespace Windows

{

namespace OPM

{

[InteropServices::Guid(

"9CAF41C2-CA86-4ffb-B05A-AC43C424D076"

)]

[InteropServices::InterfaceTypeAttribute(

InteropServices::ComInterfaceType::InterfaceIsIUnknown

)]

[InteropServices::ComVisible(true)]

publicinterfaceclass IDynamicProperty2

{

void GetGUID(

[InteropServices::Out] System::Guid% propGUID

);

void GetDisplayName(

[InteropServices::Out,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::BStr

)

] interior_ptr<System::String^> name);

void IsPropertyEnabled(

[InteropServices::In,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::IUnknown

)

] Object^ pUnk,

[InteropServices::Out] System::Int32% bEnabled

);

void IsPropertyReadOnly(

[InteropServices::Out] System::Int32% bReadonly

);

void GetDescription(

[InteropServices::Out,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::BStr

)

] interior_ptr<System::String^> description

);

void GetCurrentValueName(

[InteropServices::Out,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::BStr

)

] interior_ptr<System::String^> name

);

void GetCurrentValueType(

[InteropServices::Out] ushort% pVarType

);

void GetCurrentValueData(

[InteropServices::In,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::IUnknown

)

] Object^ pUnk,

[InteropServices::In,

InteropServices::Out,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::Struct

)

] interior_ptr<Object^> varData

);

void SetCurrentValueData(

[InteropServices::In,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::IUnknown

)

] Object^ pUnk,

[InteropServices::In,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::Struct

)

] Object^ varData

);

void Connect(

[InteropServices::In,

InteropServices::MarshalAs(

/*IDynamicPropertyNotify2*/

InteropServices::UnmanagedType::IUnknown

)

] Object^ pSink

);

void Disconnect();

};

[InteropServices::Guid(

"975112B5-5403-4197-AFB8-90C6CA73B9E1"

)]

[InteropServices::InterfaceTypeAttribute(

InteropServices::ComInterfaceType::InterfaceIsIUnknown

)]

[InteropServices::ComVisible(true)]

publicinterfaceclass IDynamicPropertyNotify2

{

void OnChanged(

[InteropServices::In,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::IUnknown

)

] Object^ pDynamicProperty

);

void GetCurrentSelectionSet(

[InteropServices::In,

InteropServices::Out,

InteropServices::MarshalAs(

InteropServices::UnmanagedType::Struct

)

] interior_ptr<Object^> pSelection

);

};

}

}

}

}

OK, that's it for the hardcore technical details on exposing AutoCAD's Properties Palette to .NET. In the next post we'll start the real fun with some real examples of using these interfaces from C# (and you can trust me when I say that's going to be a whole lot simpler than this post, honestly! :-).