In the last post we looked at some code that attaches additional data to individual entities, allowing them to be drawn independently with different visual properties via the new Overrule API in AutoCAD 2010.

A couple of comments โ€“ one from Qun, who provided the original F# sample, and one from Tony Tanzillo โ€“ have prompted me to optimize the code somewhat. Tony pointed out, very validly, that as the previous code registers its overrule against Drawable objects it will get called for every entity (and various objects besides) which could clearly impact performance. Qun pointed me to some interesting framework capabilities that allow you to let AutoCAD decide whether to call your overrule based on the existence of certain XData or some other criterion (such as the existence of a particular entry in the object's extension dictionary).

So here's what I've doneโ€ฆ

I decided to define separate overrule classes for each object type we want to handle and have these classes derive from a common abstract base class. This allows us to encapsulate all the common XData-related stuff inside the base class, and leave the child classes to worry about the type-specific display. We will then register our overrules for the specific classes they care about, which will lead them to only being called for objects of those types.

A nice feature is the ability to let AutoCAD filter on the existence of specific XData prior to invoking our overrule: check out the constructor of the PipeOverrule class โ€“ it calls SetXDataFilter() on the base DrawableOverrule class, telling AutoCAD the registered application name for which to look. An interesting test: draw some lines and/or circles, and use the MP command in the below sample to attach radii to some of them. If you set a breakpoint in your WorldDraw() functions and run the code as it stands, you'll see that they only get called for the circles/lines that have been modified by the MP command. If you comment that one line, you should see WorldDraw() being called for all circles and lines, irrespective of whether they've been given a radius or not.

Before we get into the code, here's the protocol for the DrawableOverrule class, as provided by Visual Studio:

namespace Autodesk.AutoCAD.GraphicsInterface

{

  [Wrapper("AcGiDrawableOverrule")]

  public abstract class DrawableOverrule : Overrule

  {

    protected internal DrawableOverrule();

 

    public virtual int SetAttributes(

      Drawable drawable, DrawableTraits traits);

    public override sealed void SetCustomFilter();

    public override sealed void SetExtensionDictionaryEntryFilter(

      string entryName);

    public override sealed void SetIdFilter(ObjectId[] ids);

    public override sealed void SetNoFilter();

    public override sealed void SetXDataFilter(

      string registeredApplicationName);

    public virtual void ViewportDraw(

      Drawable drawable, ViewportDraw vd);

    public virtual int ViewportDrawLogicalFlags(

      Drawable drawable, ViewportDraw vd);

    public virtual bool WorldDraw(Drawable drawable, WorldDraw wd);

  }

}

Aside from filtering on XData, we can see that we can also filter on specific ObjectIds (good for overruling objects very specifically), extension dictionary entries (good if you're working with lots of data per object and XData is no longer viable) or something more custom.

Here's the protocol for the Overrule class, the base class for DrawableOverrule:

namespace Autodesk.AutoCAD.Runtime

{

  [Wrapper("AcRxOverrule")]

  public abstract class Overrule : RXObject

  {

    public static bool Overruling { get; set; }

 

    public static void AddOverrule(

      RXClass targetClass, Overrule overrule, bool bAtLast);

    public static bool HasOverrule(

      RXObject overruledSubject, RXClass targetClass);

    public virtual bool IsApplicable(RXObject overruledSubject);

    public static void RemoveOverrule(

      RXClass targetClass, Overrule overrule);

    public abstract void SetCustomFilter();

    public abstract void SetExtensionDictionaryEntryFilter(

      string entryName);

    public abstract void SetIdFilter(ObjectId[] ids);

    public abstract void SetNoFilter();

    public abstract void SetXDataFilter(

      string registeredApplicationName);

 
; }

}

The protocol in which we're interested has been declared as abstract (== pure virtual) at this level, so we need to use DrawableOverrule as a base class unless we want to implement the protocol ourselves. One additional function to be overridden at this level is IsApplicable(), which is the fundamental way the overrule tells AutoCAD whether it can work with a specific object, or not. This is the function we would override to implement a custom filter, and is the only part of the filter protocol available from ObjectARX: more work has been done on the .NET side of things, which thankfully makes our lives much simpler. ๐Ÿ™‚

One open question in my mind: while asking AutoCAD to filter on XData is simpler and means our code is called less often, it does mean that both AutoCAD and our code will be checking for XData (when we retrieve the radius information). Presumably AutoCAD is going to check very efficiently, which means the code should be quicker in the cases where we have lots of objects that are not overruled, but in the cases where we are overruling a lot of objects there does appear to be some potential for this making the application run marginally slower. Ultimately โ€“ as mentioned earlier - there's just a single line of code to comment out if you want to see the difference (and presumably profile the performance in some way). Just something to watch for, in case โ€“ I doubt this is something that's going to have a significant โ€“ or even measurable โ€“ performance impact, but anyway.

Here's the modified C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.GraphicsInterface;

using Autodesk.AutoCAD.Colors;

 

namespace DrawOverrule

{

  public abstract class PipeOverrule : DrawableOverrule

  {

    const string regAppName = "TTIF_PIPE";

 

    public PipeOverrule()

    {

      // Tell AutoCAD to filter on our application name

      // (this means our overrule will only be called

      // on objects possessing XData with this name)

 

      SetXDataFilter(regAppName);

    }

 

    // Get the XData for a particular object

    // and return the "pipe radius" if it exists

 

    public static double PipeRadiusForObject(DBObject obj)

    {

      double res = 0.0;

 

      ResultBuffer rb = obj.XData;

      if (rb != null)

      {

        bool foundStart = false;

 

        foreach (TypedValue tv in rb)

        {

          if (tv.TypeCode == (int)DxfCode.ExtendedDataRegAppName &&

              tv.Value.ToString() == regAppName)

            foundStart = true;

          else

          {

            if (foundStart == true)

            {

              if (tv.TypeCode == (int)DxfCode.ExtendedDataReal)

              {

                res = (double)tv.Value;

                break;

              }

            }

          }

        }

        rb.Dispose();

      }

      return res;

    }

 

    // Set the "pipe radius" in the XData of a particular object

 

    public static void SetPipeRadiusOnObject(

      Transaction tr, DBObject obj, double radius

    )

    {

      Database db = obj.Database;

 

      // Make sure the application is registered

      // (we could separate this out to be called

      // only once for a set of operations)

 

      RegAppTable rat =

        (RegAppTable)tr.GetObject(

          db.RegAppTableId,

          OpenMode.ForRead

        );

 

      if (!rat.Has(regAppName))

      {

        rat.UpgradeOpen();

        RegAppTableRecord ratr = new RegAppTableRecord();

        ratr.Name = regAppName;

        rat.Add(ratr);

        tr.AddNewlyCreatedDBObject(ratr, true);

      }

 

      // Create the XData and set it on the object

 

      ResultBuffer rb =

        new ResultBuffer(

          new TypedValue(

            (int)DxfCode.ExtendedDataRegAppName, regAppName

          ),

          new TypedValue(

            (int)DxfCode.ExtendedDataReal, radius

          )

        );

      obj.XData = rb;

      rb.Dispose();

    }

  }

 

  // An overrule to make a pipe out of line

 

  public class LinePipeOverrule : PipeOverrule

  {

    static public LinePipeOverrule theOverrule =

      new LinePipeOverrule();

 

    private SweepOptions sweepOpts = new SweepOptions();

 

    public override bool WorldDraw(Drawable d, WorldDraw wd)

    {

      double radius = 0.0;

 

      if (d is DBObject)

        radius = PipeRadiusForObject((DBObject)d);

 

      if (radius > 0.0)

      {

        Line line = d as Line;

 

        if (line != null)

        {

          // Draw the line as is, with overruled attributes

 

          base.WorldDraw(line, wd);

          if (!line.Id.IsNull && line.Length > 0.0)

          {

            // Draw a pipe around the line

 

            EntityColor c =

              wd.SubEntityTraits.TrueColor;

            wd.SubEntityTraits.TrueColor =

              new EntityColor(0x00AfAfff);

            wd.SubEntityTraits.LineWeight =

              LineWeight.LineWeight000;

            Circle clr =

              new Circle(

                line.StartPoint,

                line.EndPoint - line.StartPoint,

                radius

              );

            ExtrudedSurface pipe = new ExtrudedSurface();

            try

            {

              pipe.CreateExtrudedSurface(

                clr, line.EndPoint - line.StartPoint, sweepOpts

              );

            }

            catch

            {

              Document doc =

                Application.DocumentManager.MdiActiveDocument;

              doc.Editor.WriteMessage(

                "\nFailed with CreateExtrudedSurface."

              );

            }

            clr.Dispose();

            pipe.WorldDraw(wd);

            pipe.Dispose();

            wd.SubEntityTraits.TrueColor = c;

          }

          return true;

        }

      }

      return base.WorldDraw(d, wd);

    }

 

    public override int SetAttributes(Drawable d, DrawableTraits t)

    {

      int b = base.SetAttributes(d, t);

 

      double radius = 0.0;

 

      if (d is DBObject)

        radius = PipeRadiusForObject((DBObject)d);

 

      if (radius > 0.0)

      {

        // Set color to index 6

 

        t.Color = 6;

 

        // and lineweight to .40 mm

 

        t.LineWeight = LineWeight.LineWeight040;

      }

      return b;

    }

  }

 

  // An overrule to make a pipe out of circle

 

  public class CirclePipeOverrule : PipeOverrule

  {

    static public CirclePipeOverrule theOverrule =

      new CirclePipeOverrule();

 

    private SweepOptions sweepOpts = new SweepOptions();

 

    public override bool WorldDraw(Drawable d, WorldDraw wd)

    {

      double radius = 0.0;

 

      if (d is DBObject)

        radius = PipeRadiusForObject((DBObject)d);

 

      if (radius > 0.0)

      {

        Circle circle = d as Circle;

 

        if (circle != null)

        {

          // Draw the circle as is, with overruled attributes

 

          base.WorldDraw(circle, wd);

 

          // Needed to avoid ill-formed swept surface

 

          if (circle.Radius > radius)

          {

            // Draw a pipe around the cirle

 

            EntityColor c = wd.SubEntityTraits.TrueColor;

            wd.SubEntityTraits.TrueColor =

              new EntityColor(0x3fffe0e0);

            wd.SubEntityTraits.LineWeight =

              LineWeight.LineWeight000;

            Vector3d normal =

              (circle.Center - circle.StartPoint).

                CrossProduct(circle.Normal);

            Circle clr =

              new Circle(

                circle.StartPoint, normal, radius

              );

            SweptSurface pipe = new SweptSurface();

            pipe.CreateSweptSurface(clr, circle, sweepOpts);

            clr.Dispose();

            pipe.WorldDraw(wd);

            pipe.Dispose();

            wd.SubEntityTraits.TrueColor = c;

          }

          return true;

        }

      }

      return base.WorldDraw(d, wd);

    }

 

    public override int SetAttributes(Drawable d, DrawableTraits t)

    {

      int b = base.SetAttributes(d, t);

 

      double radius = 0.0;

 

      if (d is DBObject)

        radius = PipeRadiusForObject((DBObject)d);

 

      if (radius > 0.0)

      {

        // Set color to index 2

 

        t.Color = 2;

 

        // and lineweight to .60 mm

 

        t.LineWeight = LineWeight.LineWeight060;

      }

      return b;

    }

  }

< p style="margin: 0px; font-size: 8pt"> 

  public class Commands

  {

    private double _radius = 0.0;

 

    public void Overrule(bool enable)

    {

      // Regen to see the effect

      // (turn on/off Overruling and LWDISPLAY)

 

      DrawableOverrule.Overruling = enable;

      if (enable)

        Application.SetSystemVariable("LWDISPLAY", 1);

      else

        Application.SetSystemVariable("LWDISPLAY", 0);

 

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

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

      doc.Editor.Regen();

    }

 

    [CommandMethod("OVERRULE1")]

    public void OverruleStart()

    {

      ObjectOverrule.AddOverrule(

        RXClass.GetClass(typeof(Line)),

        LinePipeOverrule.theOverrule,

        true

      );

      ObjectOverrule.AddOverrule(

        RXClass.GetClass(typeof(Circle)),

        CirclePipeOverrule.theOverrule,

        true

      );

      Overrule(true);

    }

 

    [CommandMethod("OVERRULE0")]

    public void OverruleEnd()

    {

      ObjectOverrule.RemoveOverrule(

        RXClass.GetClass(typeof(Line)),

        LinePipeOverrule.theOverrule

      );

      ObjectOverrule.RemoveOverrule(

        RXClass.GetClass(typeof(Circle)),

        CirclePipeOverrule.theOverrule

      );

      Overrule(false);

    }

 

    [CommandMethod("MP", CommandFlags.UsePickSet)]

    public void MakePipe()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

 

      // Ask the user to select the entities to make into pipes

 

      PromptSelectionOptions pso =

        new PromptSelectionOptions();

      pso.AllowDuplicates = false;

      pso.MessageForAdding =

        "\nSelect objects to turn into pipes: ";

 

      PromptSelectionResult selRes =

        doc.Editor.GetSelection(pso);

 

      // If the user didn't make valid selection, we return

 

      if (selRes.Status != PromptStatus.OK)

        return;

 

      SelectionSet ss = selRes.Value;

 

      // Ask the user for the pipe radius to set

 

      PromptDoubleOptions pdo =

        new PromptDoubleOptions(

          "\nSpecify pipe radius:"

        );

 

      // Use the previous value, if if already called

 

      if (_radius > 0.0)

      {

        pdo.DefaultValue = _radius;

        pdo.UseDefaultValue = true;

      }

      pdo.AllowNegative = false;

      pdo.AllowZero = false;

 

      PromptDoubleResult pdr =

        ed.GetDouble(pdo);

 

      // Return if something went wrong

 

      if (pdr.Status != PromptStatus.OK)

        return;

 

      // Set the "last radius" value for when

      // the command is called next

 

      _radius = pdr.Value;

 

      // Use a transaction to edit our various objects

 

      Transaction tr =

        db.TransactionManager.StartTransaction();

      using (tr)

      {

        // Lop through the selected objects

 

        foreach (SelectedObject o in ss)

        {

          // We could choose only to add XData to the objects

          // we know will use it (Lines and Circles, for now)

 

          DBObject obj =

            tr.GetObject(o.ObjectId, OpenMode.ForWrite);

          PipeOverrule.SetPipeRadiusOnObject(tr, obj, _radius);

        }

        tr.Commit();

      }

    }

  }

}

In terms of what happens when we run the code, here are the results from the previous post (as it should function identically).

First some simple geometry for which we're going to overrule the display:

2D wireframe display of individual lines and circles

And here's some geometry for which we've attached radii using the MP command, once we've turned our overrules on via the OVERRULE1 command:

3D conceptual display of overruled individual lines and circles

8 responses to “Optimized overruling in AutoCAD 2010 using .NET”

  1. Hi Kean,

    Excellent series of posts. It's exciting to see a very usable implementation of 'custom' objects in .NET. Obviously, this implementation protects what could be proprietary business intelligence from being sent around, but it brings up a question. What process would you use to 'explode' these elements so that you could send the drawing to someone without your code, but with the custom elements in it.

    Thanks!
    -Danny

  2. Hi Danny,

    Great question!

    You can implement a TransformOverrule, implementing the Explode() method, which will allow your object to participate in an EXPLODE.

    I'll cover this in the next post. ๐Ÿ™‚

    Kean

  3. Kudos and thanks again for another excellent example.

    One suggestion, the 'REGEN3' command can actually be written in managed code quite easily (it's basically just a call to AcGsModel::invalidate() and a redraw).

    I've added that code to the latest update of the sample I posted at the link below.

    caddzone.com/Ove...

  4. Hi Kean,

    For various reasons I've got loads of code in VBA that cannot be used for much longer with AutoCAD. VBA ships with my Civil 3D 2010, but will it be there in 2011?
    I need to move over to .NET, that is clear, but what language would you recommend?
    Is there an easy way of translating VBA code into .NET code or do I have to do it all manually and theres lots of it!?

    Your advice would be most welcome.

    Ben

  5. Hi Ben,

    I don't know what the plan is for VBA in the next release of Civil 3D (and wouldn't be able to communicate it publicly, if I did).

    About migrating away from VBA, in general terms...

    On the one hand I wouldn't count on VBA disappearing completely in the near term (for most of our products it is available as a separate download and I don't know of plans to stop doing that), but on the other hand I think moving away from VBA makes a lot of sense (as much as anything its partial support on 64-bit platforms will cause you performance problems once Civil 3D becomes available as a native 64-bit version).

    As for moving to .NET... the logical language to move to from VBA is VB.NET. As .NET languages have great support for COM, the simplest first step is to auto-migrate the code to call through COM rather than the managed APIs. The migration wizard built into Visual Studio apparently does a great job of this, by the way, the problem is that it only accepts VB6 projects, not VBA.

    A member of my team, Stephen Preston, is working on a tool and an accompanying DevTV session to demonstrate how to migrate from VBA to VB.NET (the tool actually converts VBA to VB6 projects, which can then be loaded into the migration wizard). These should be posted here in the coming weeks.

    Regards,

    Kean

  6. Hi Kean,

    Thanks for the advice.

    I've used the migration wizard in Visual Studio a couple of times on VB6 code with mixed success, it can deal with the vast majority of the code directly and marks up the parts that need 'polishing up' by hand.

    The tool from Stephen will be a great help and save me loads of time and sweat. Will it also deal with the forms as well as the code?

    I've been using VB.NET for stand alone applications for a long time, but as yet I've not used it with AutoCAD or Civil 3D. I have a couple of books somewhere with advice on writing VB for AutoCAD, I guess now's the time to dust them off ๐Ÿ˜‰

    Thanks again and I'm looking forward to Stephen's post.

    Ben

  7. I believe it will indeed handle forms, but I haven't yet seen it in action...

    Kean

  8. Tony,

    Thanks - that's a great tip!

    Kean

Leave a Reply to Danny Cancel reply

Your email address will not be published. Required fields are marked *