Wednesday, June 22, 2011

Show/Hide Segments of a Polyline With Overrule

I saw a question posted in AutoCAD .NET forum asking "if it's possible to create a polyline with invisible segment" here. The question raised my curiosity of thinking how do I do it. Right away, I thought Overrule would be the easy approach to do it. Thus the code shows in this article.

Some explanations. To make things simple, I decide to draw a polyline with multiple segments (e.g. with more than 2 vertices) so that every other segment is visible; also, the Overrule would only apply to polylines that are on certain layer; and the Overrule itself can be turn on or off, regardless if there is other custom Overrule taking effect.

Here is the 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;
 
[assembly: CommandClass(typeof(HidePartialPLine.MyCommands))]
 
namespace HidePartialPLine
{
    public class MyCommands 
    {
        private const string OR_LAYER="Overruled_PLine";
        private static MyPLineOverrule _myOR=null;
 
        [CommandMethod("MyOR")]
        public static void ToggleMyOverrule()
        {
            Document dwg = Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
 
            if (_myOR==null) 
            {
                _myOR=new MyPLineOverrule();
                _myOR.StartOverrule();
                ed.WriteMessage("\nMyPLineOverrule is on.\n");
            }
            else
            {
                if (!_myOR.OverruleOn) 
                {
                    _myOR.StartOverrule();
                    ed.WriteMessage("\nMyPLineOverrule is on.\n");
                }
                else
                {
                    _myOR.StopOverrule();
                    ed.WriteMessage("\nMyPLineOverrule is off.\n");
                }
            }    
        }
 
        [CommandMethod("SetOR")]
        public static void SetMyOverruleToPolyline()
        {
            Document dwg = Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
 
            ObjectId id = SelectPolyline(ed);
            if (id == ObjectId.Null)
            {
                ed.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            SetPolylineLayer(id, OR_LAYER, dwg.Database);
        }
 
        #region private methods
 
        private static ObjectId SelectPolyline(Editor ed)
        {
            PromptEntityOptions opt=new PromptEntityOptions("\nPick a polyline");
            opt.SetRejectMessage("You must pick a polyline. ");
            opt.AddAllowedClass(
                typeof(Autodesk.AutoCAD.DatabaseServices.Polyline),true);
 
            PromptEntityResult res=ed.GetEntity(opt);
            if (res.Status==PromptStatus.OK)
            {
                return res.ObjectId;
            }
            else
            {
                return ObjectId.Null;
            }
        }
 
        private static void SetPolylineLayer(
            ObjectId entId, string layerName, Database db)
        {
            using (Transaction tran = db.TransactionManager.StartTransaction())
            {
                Entity ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
                if (ent.Layer.ToUpper() != OR_LAYER.ToUpper())
                {
                    ent.UpgradeOpen();
                    ent.Layer = OR_LAYER;
                }
                tran.Commit();
            }
        }
 
        #endregion
    }
 
    public class MyPLineOverrule : DrawableOverrule
    {
        private const string OR_LAYER = "Overruled_PLine";
        private bool _overruling=false;
        private bool _oldOverruling=false;
 
        public MyPLineOverrule()
        {
            _overruling=false;
        }
 
        public bool OverruleOn
        {
            get { return _overruling; }
        }
 
        public void StartOverrule()
        {
            _overruling=true;
            _oldOverruling = Overrule.Overruling;
 
            Overrule.AddOverrule(RXObject.GetClass(
                typeof(Autodesk.AutoCAD.DatabaseServices.Polyline)), 
                this, false);
 
            //Set custom filter, so that code in overriden IsApplicable() 
            //will run to do the filtering
            this.SetCustomFilter();
 
            Overrule.Overruling=true;
 
            Application.DocumentManager.MdiActiveDocument.Editor.Regen();
        }
 
        public void StopOverrule()
        {
            _overruling = false;
 
            //Cause Overrule redraw the polyline to its original shape
            //before the overrule is stopped
            Application.DocumentManager.MdiActiveDocument.Editor.Regen();
 
            Overrule.RemoveOverrule(RXObject.GetClass(
                typeof(Autodesk.AutoCAD.DatabaseServices.Polyline)), this);
 
            Overrule.Overruling=_oldOverruling;
        }
 
        #region Override overrule methods
 
        public override bool  IsApplicable(RXObject overruledSubject)
        {
            //Only apply this overrule to polyline on layer "Overruled_PLine"
            Entity ent=overruledSubject as Entity;
            return ent.Layer.ToUpper() == OR_LAYER.ToUpper();
        }
 
        public override bool  WorldDraw(Drawable drawable, WorldDraw wd)
        {
            Autodesk.AutoCAD.DatabaseServices.Polyline pl = drawable
                as Autodesk.AutoCAD.DatabaseServices.Polyline;
 
            //If the polyline only has one segment, draw itself
            if (pl.NumberOfVertices < 3)
            {
                return base.WorldDraw(drawable, wd);
            }
 
            if (_overruling)
            {
                //Get polyline's vertices
                Point3d[] pts = new Point3d[pl.NumberOfVertices];
                for (int i = 0; i < pl.NumberOfVertices; i++)
                {
                    pts[i] = pl.GetPoint3dAt(i);
                }
 
                //Draw every other segement
                int n = 0;
                while (n < pts.Length)
                {
                    wd.Geometry.WorldLine(pts[n], pts[n + 1]);
                    n += 2;
                }
 
                return true;
            }
            else
            {
                return base.WorldDraw(drawable, wd);
            }
        }
 
        #endregion
    }
}

The code pretty much explains itself. To test the code, I started AutoCAD, created a layer named as "Overruled_PLine". Then I drew a few polylines on layer 0. After the code being "netload"ed, I can toggle MyPLineOverrule on/off. When it is on, move polyline to layer "Overruled_PLine" will make the segments of a polyline visible/invisible alternately.

The effect can be watched in this video clip.

Saturday, June 18, 2011

Set Layouts in Order

When creating a new layout in AutoCAD, the newly created layout is appended to AutoCAD drawing editor as last layout tab. Sometimes, a user may want to sort the layout tabs in certain order, such as ascending/descending by the layout name, or whatever order the user wants.

AutoCAD editor itself provides a way to manually sort layout tab by right-clicking a layout tab and select "Move or copy..." context menu. This way, user can change a layout tab's order one at a time. It would be tedious work if the user has to re-order many layout tabs.

Sometimes, one may create many layouts programmatically and want to sort their tabs in certain way, most likely, sort them by layout names.

Here is some code that sorts layout tabs by the layout names. The code actually does is to get a string list/array from all layouts' name, then sort/re-order the string list/array, and finally set a Layout's TabOrder property according to its name's order in the string list/array.

    1 using System.Collections.Generic;
    2 using System.Linq;
    3 
    4 using Autodesk.AutoCAD.ApplicationServices;
    5 using Autodesk.AutoCAD.DatabaseServices;
    6 using Autodesk.AutoCAD.Runtime;
    7 using Autodesk.AutoCAD.EditorInput;
    8 
    9 [assembly: CommandClass(typeof(OrderLayout.MyCommands))]
   10 
   11 namespace OrderLayout
   12 {
   13     public class MyCommands
   14     {
   15         [CommandMethod("OrderLayout")]
   16         public static void OrderAllLayouts()
   17         {
   18             Document dwg = Application.DocumentManager.MdiActiveDocument;
   19             Editor ed = dwg.Editor;
   20 
   21             //Get user input of sorting direction
   22             PromptKeywordOptions opt =
   23                 new PromptKeywordOptions("\nEnter sorting order: ");
   24             opt.Keywords.Add("Asc");
   25             opt.Keywords.Add("Desc");
   26             opt.AppendKeywordsToMessage = true;
   27 
   28             PromptResult res = ed.GetKeywords(opt);
   29             if (res.Status == PromptStatus.OK)
   30             {
   31                 string order = res.StringResult;
   32 
   33                 //Sort the layouts
   34                 SetLayoutOrder(order);
   35 
   36                 ed.Regen();
   37             }
   38             else
   39             {
   40                 ed.WriteMessage("\n*Cancel*\n");
   41             }
   42         }
   43 
   44         [CommandMethod("NewLayout")]
   45         public static void CreateNewLayout()
   46         {
   47             Document dwg = Application.DocumentManager.MdiActiveDocument;
   48             Editor ed = dwg.Editor;
   49 
   50             //Get new layout name from user
   51             PromptStringOptions opt =
   52                 new PromptStringOptions("\nEnter a new layout name: ");
   53 
   54             PromptResult res = res = ed.GetString(opt);
   55             if (res.Status == PromptStatus.OK)
   56             {
   57                 string lName = res.StringResult;
   58 
   59                 //Create new layout
   60                 CreateLayout(lName);
   61 
   62                 ed.Regen();
   63             }
   64             else
   65             {
   66                 ed.WriteMessage("\n*Cancel*\n");
   67             }
   68         }
   69 
   70         [CommandMethod("ClearLayout")]
   71         public static void ClearAllLayout()
   72         {
   73             Document dwg = Application.DocumentManager.MdiActiveDocument;
   74             Editor ed = dwg.Editor;
   75 
   76             //Remove all layout.
   77             //Note, depending Acad configuration, after this method running,
   78             //all layouts should be removed. However, AutoCAD may always create
   79             //a layout named as "Layout1" right after this command execution.
   80             ClearLayouts();
   81 
   82             ed.WriteMessage("All layouts have been removed.");
   83         }
   84 
   85         #region private methods - miscellaneous, create and remove layout
   86 
   87         //This method returns a layout name list ("Model" excluded)
   88         private static string[] GetLayoutNames()
   89         {
   90             List<Layout> lays = new List<Layout>();
   91 
   92             Database db = HostApplicationServices.WorkingDatabase;
   93             using (Transaction tran = db.TransactionManager.StartTransaction())
   94             {
   95                 DBDictionary lDic =
   96                     (DBDictionary)tran.GetObject(db.LayoutDictionaryId,
   97                     OpenMode.ForRead);
   98 
   99                 foreach (DBDictionaryEntry entry in lDic)
  100                 {
  101                     ObjectId id = (ObjectId)entry.Value;
  102                     lays.Add(tran.GetObject(id, OpenMode.ForRead) as Layout);
  103                 }
  104 
  105                 tran.Commit();
  106             }
  107 
  108             int c = lays.Count;
  109 
  110             return (from l in lays
  111                     where l.LayoutName.ToUpper() != "MODEL"
  112                     select l.LayoutName).ToArray();
  113         }
  114 
  115         private static Layout GetLayoutFromId(ObjectId id, Transaction tran)
  116         {
  117             return tran.GetObject(id, OpenMode.ForRead) as Layout;
  118         }
  119 
  120         private static void CreateLayout(string layoutName)
  121         {
  122             //Get Layout list
  123             string[] layouts = GetLayoutNames();
  124             if (LayoutExists(layoutName, layouts)) return;
  125 
  126             LayoutManager mgr = LayoutManager.Current;
  127             mgr.CreateLayout(layoutName);
  128         }
  129 
  130         private static void ClearLayouts()
  131         {
  132             //Get Layout list
  133             string[] layouts = GetLayoutNames();
  134 
  135             LayoutManager mgr = LayoutManager.Current;
  136             foreach (string l in layouts)
  137             {
  138                 mgr.DeleteLayout(l);
  139             }
  140         }
  141 
  142         private static bool LayoutExists(string lName, string[] layouts)
  143         {
  144             return (from l in layouts
  145                     where l.ToUpper() == lName.ToUpper()
  146                     select l).Count() > 0;
  147         }
  148 
  149         #endregion
  150 
  151         #region private methods - Sort layout in order
  152 
  153         private static void SetLayoutOrder(string order)
  154         {
  155             //Get Layout list
  156             string[] layouts = GetLayoutNames();
  157             string[] sortedLayouts;
  158 
  159             if (order.ToUpper() == "ASC")
  160                 sortedLayouts = (from l in layouts
  161                                  orderby l ascending select l).ToArray();
  162             else
  163                 sortedLayouts = (from l in layouts
  164                                  orderby l descending select l).ToArray();
  165 
  166             Database db = HostApplicationServices.WorkingDatabase;
  167 
  168             using (Transaction tran = db.TransactionManager.StartTransaction())
  169             {
  170                 LayoutManager mgr = LayoutManager.Current;
  171 
  172                 //First layout tab order should be 1
  173                 //because "Model" layout tab's order is always 0
  174                 int tab = 1;
  175                 foreach (string l in sortedLayouts)
  176                 {
  177                     ObjectId id = mgr.GetLayoutId(l);
  178 
  179                     Layout lay = tran.GetObject(id, OpenMode.ForWrite) as Layout;
  180                     lay.TabOrder = tab;
  181 
  182                     tab++;
  183                 }
  184 
  185                 tran.Commit();
  186             }
  187         }
  188 
  189         #endregion
  190     }
  191 }

In the above vode, command "NewLayout" and "ClearLayout" are just for the convenience of testing the "OrderLayout" command. To see the layout sorting effect, use "NewLayout" command to create a few randomly named layouts. Then run "OrderLayout" command.

As you can see, the trick of sorting layouts lies in getting a layout name list and re-order it in a desired order, then setting Layout.TabOrder property according to its name's order in the re-ordered layout name list. If you need to sort the layouts in specific order (not just ascending or descending by its name), you may want to build a UI that allows user to line up layouts' name in any order he/she wants to. Then you can obtain a String list/array ordered in the order user sets. Afterwards, you know how to set Layout's TabOrder property, as the code shows here.

Thursday, April 7, 2011

Tips of Using LINQ in AutoCAD .NET programming

If you target your AutoCAD .NET API programming at .NET3.5 or 4.0, using LINQ would sometimes make your coding a lot easier. Typically, writing code to loop through various collection objects in drawing database is one of mostly encountered tasks of AutoCAD programmers.

I demonstrate here some sample code that uses LINQ (to object) to make the code simpler and easier.

BlockTableRecord class is one the mostly used class an AutoCAD programmer may have to deal with. It implements IEnumerale interface. So, we have seen a lots of "foreach(...)" loop used against it in AutoCAD .NET code, especially when ModelSpace BlockTableRecord is opened for searching certain type of entities contained in ModelSpace. Here are some cases.

Case 1:  searching certain types of entities in BlockTableRecord (ModelSpace, PaperSpace, or a block definition).

private List<ObjectId> GetCertainEntityIDs(Database db)
{
    List<ObjectId> ids = null;
 
    using (Transaction tran = db.TransactionManager.StartTransaction())
    {
        BlockTable tbl = 
            (BlockTable)tran.GetObject(db.BlockTableId, OpenMode.ForRead);
 
        //Get modelspace BloclTableRecord
        BlockTableRecord br = 
            (BlockTableRecord)tran.GetObject(tbl[BlockTableRecord.ModelSpace], OpenMode.ForRead);
 
        //Cast the BlockTableRecord into IEnumeralbe<T> collection 
        IEnumerable<ObjectId> b = br.Cast<ObjectId>();
 
        //==============search certain entity========================//
        //"LINE" for line
        //"LWPOLYLINE" for polyline
        //"CIRCLE" for circle
        //"INSERT" for block reference
        //...
        //We can use "||" (or) to search for more then one entity types
        //============================================================//
 
        //Use lambda extension method
        ids = b.Where(id => id.ObjectClass.DxfName.ToUpper() == "LINE" || 
            id.ObjectClass.DxfName.ToUpper() == "LWPOLYLINE").ToList<ObjectId>();
 
        //Use LINQ statement. This is more readable
        ids = (from id in b
                where id.ObjectClass.DxfName.ToUpper()=="LINE" ||
                        id.ObjectClass.DxfName.ToUpper() == "LWPOLYLINE"
                select id).ToList<ObjectId>();
 
        tran.Commit();
    }
 
    return ids;
}

Case 2: often, we need to get a distinctive entity type list from a drawing (e.g. we need to know what types of entities the drawing's model space contains). Following code get a string array of entity types from model space.

private static string[] GetEntityType(Document dwg)
{
    string[] types = null;
 
    Database db = dwg.Database;
    using (Transaction tran = db.TransactionManager.StartTransaction())
    {
        BlockTable tbl = 
            (BlockTable)tran.GetObject(db.BlockTableId, OpenMode.ForRead);
 
        //Get modelspace BloclTableRecord
        BlockTableRecord br = 
            (BlockTableRecord)tran.GetObject(tbl[BlockTableRecord.ModelSpace], OpenMode.ForRead);
 
        //Cast the BlockTableRecord into IEnumeralbe<T> collection
        IEnumerable<ObjectId> b = br.Cast<ObjectId>();
 
        //Use lambda extension method
        types = b.Select(id => id.ObjectClass.DxfName).Distinct().ToArray();
 
        //Use LINQ statement. This is more readable
        types = (from id in b select id.ObjectClass.DxfName).Distinct().ToArray();
 
        tran.Commit();
    }
 
    return types;
}

Case 3: getting a name list of blocks that have been inserted into model space, or get all block references or block references with given names in model space.

From the code show above, we can see ObjectClass property of ObjectId struct, which was only made available since AutoCAD 2009, helps a lot in defining searching condition (in Where() or Distinct() extension methods). In this scenario, we need a extra method to get to entity itself. Here is it:

private static BlockReference GetBlockRef(ObjectId id, Transaction tran)
{
    return (tran.GetObject(id, OpenMode.ForRead) as BlockReference);
}

Now we can get a name list of all inserted blocks:

private static string[] GetBlockNameList(Document dwg)
{
    string[] names = null;
 
    Database db = dwg.Database;
    using (Transaction tran = db.TransactionManager.StartTransaction())
    {
        BlockTable tbl = 
            (BlockTable)tran.GetObject(db.BlockTableId, OpenMode.ForRead);
 
        //Get modelspace BloclTableRecord
        BlockTableRecord br = 
            (BlockTableRecord)tran.GetObject(tbl[BlockTableRecord.ModelSpace], OpenMode.ForRead);
 
        //Cast the BlockTableRecord into IEnumeralbe<T> collection
        IEnumerable<ObjectId> b = br.Cast<ObjectId>();
 
        //Use lambda extension method
        names=b.Where(id=>id.ObjectClass.DxfName.ToUpper()=="INSERT")
                .Select(id=>GetBlockRef(id,tran).Name).Distinct().ToArray();
 
        //Use LINQ statement. This is more readable
        names = (from id in b
                where id.ObjectClass.DxfName.ToUpper().Contains("INSERT")
                select GetBlockRef(id, tran).Name).Distinct().ToArray();
 
        tran.Commit();
    }
 
    return names;
}

To search blocks with given name, we do this:

private static List<BlockReference> GetBlockRefs(string blkName, Document dwg)
{
    List<BlockReference> blks = null;
 
    Database db = dwg.Database;
    using (Transaction tran = db.TransactionManager.StartTransaction())
    {
        BlockTable tbl = 
            (BlockTable)tran.GetObject(db.BlockTableId, OpenMode.ForRead);
 
        //Get modelspace BloclTableRecord
        BlockTableRecord br = 
            (BlockTableRecord)tran.GetObject(tbl[BlockTableRecord.ModelSpace], OpenMode.ForRead);
 
        //Cast the BlockTableRecord into IEnumeralbe<T> collection
        IEnumerable<ObjectId> b = br.Cast<ObjectId>();
 
        //Use lambda extension method
        blks = b.Where(id => id.ObjectClass.DxfName.ToUpper()=="INSERT")
            .Select(id => GetBlockRef(id, tran)).ToList<BlockReference>();
 
        //Use LINQ statement. This is more readable
        blks = (from id in b
                where id.ObjectClass.DxfName.ToUpper()=="INSERT"
                select GetBlockRef(id, tran)).ToList<BlockReference>();
 
        tran.Commit();
    }
 
    //return only blockreferences with name starting with blkName with lambda extension method
    return blks.Where(b=>b.Name.ToUpper()==blkName.ToUpper()).ToList();
 
    //Or return with LINQ statement
    return (from b in blks where b.Name.ToUpper()==blkName.ToUpper() select b).ToList();
}

Note, the last code sample "GetBlockRefs()" is only for demonstrating the use of LINQ. It may not be a good idea in certain situation to return entity instead of ObjectId outside a transaction.

As you can see, with the help of LINQ, we can write more readable code easily to search drawing database. However, what is the difference in term of code execution speed between simply looping through the model space with foreach (...) and using LINQ as I showed here? I did not look into this, honestly.I did run the code with drawing with a few thousands entities in model space and did not feel speed difference. But I assume there is difference with LINQ code might result in slightly slower speed: when casting BlockTableRecord into IEnumerable, a loop must have been done behind the scene. If you have very big model with hundreds of thousands entities, you want to test if the LINQ code actually slows down the execution noticeably.

Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.