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
{
public class CommandsAndFunctions
{
[CommandMethod("-PYLOAD")]
public static void PythonLoadCmdLine()
{
PythonLoad(true);
}
[CommandMethod("PYLOAD")]
public static void PythonLoadUI()
{
PythonLoad(false);
}
public static void 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 =
new PromptOpenFileOptions(
"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")]
public ResultBuffer PythonLoadLISP(ResultBuffer rb)
{
const int 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 ?
new ResultBuffer(
new TypedValue(RTSTR, tv.Value)
)
: null);
}
}
return null;
}
private static bool 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. :-)