March 23, 2009

Jigging an AutoCAD solid using IronPython and .NET

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. :-)