Displaying different entities in AutoCAD viewports using .NET

This is a problem that developers have been struggling with for some time, and it came up again at the recent Cloud Accelerator: how to control the display of AutoCAD geometry at a per-viewport level, perhaps to implement your own "isolate in a viewport" command.

It's certainly possible to control layer visibility at the viewport level, of course, but this is sometimes at odds with how users wish to use layers for their own purposes. An application may want to isolate geometry in a certain location from a number of layers, for instance, and it becomes cumbersome to hijack the layer system and change everything back, afterwards.

So on the way back from Prague, I started work on an overrule that does this cleanly. Attempts have been made before – here's one I found out about fairly recently that didn't work for me – but I think the combination of tricks I put together here should be useful for people.

To start with, I decided to create a DrawableOverrule that does a few things: for all AutoCAD entities, it makes sure SetAttributes() indicates that the entities need to be drawn per-viewport. It also returns false from WorldDraw(), forcing ViewportDraw() to be called. Because a lot of AutoCAD entities don't implement ViewportDraw(), we also need to capture the WorldDraw() graphics and send them through to ViewportDraw(). So we've derived a "pass through" class from WorldDraw (and a corresponding geometry object from WordGeometry) that encapsulates a ViewportDraw object (and its ViewportGeometry) and simply makes calls through to these, as needed.

There were a few nasty little gotchas, along the way, though…

Firstly, the Shell() call was failing, as one of the arguments passed in to it couldn't be marshalled across to the equivalent ViewportGeometry call. Very strange. But creating a new collection and copying the contents into it addressed that.

Secondly, the regen-type specific geometry for solids and surfaces wasn't being collected properly by the graphics system. Usually it makes two passes, one with "shaded" regen-type and the other with "standard" regen-type. This is enough to display the graphics appropriately for the various AutoCAD visual styles. In this case both calls were made with "standard", so we needed some code to track the calls made for each drawable and force "shaded" for the first of each set of calls. I'm hopeful this is something that won't be needed, in the future, but for now it's a required workaround. Many thanks for Erik Larsen for his help identifying both the issue and how to deal with it.

I've only attached the event handler that cleans the cache needed by this workaround at the active document's command boundaries. I was lazy: if you're implementing this yourself, please make sure you attach the event for each open document (and any opened/created in the future).

Block attribute support was also pretty tricky to get working: I realised last night that I needed to attach the overrule at the DBObject level – not the Entity level – as BlockTableRecords are drawable and responsible for drawing constant attribute definitions. This was the "aha!" that allowed me to get this bit working (along with some trial and error with SetAttributes() flags).

Here's the code in action against a test project. When the PVE command toggles the overrule on, the lower-left viewport will show everything while the other three should show a variety of object types. You can control this very easily by adjusting the logic in the IsVisible() function in the code: right now it works on object type but you could easily store a list of ObjectIds and use that, instead.

Per-viewport entity display

And here's the C# code that enables the PVE command:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.Colors;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.GraphicsInterface;

using Autodesk.AutoCAD.Runtime;

using AcGi = Autodesk.AutoCAD.GraphicsInterface;

using AcDb = Autodesk.AutoCAD.DatabaseServices;

using System;

using System.Collections.Generic;

 

namespace PerViewportEntityDisplay

{

  public static class Extensions

  {

    /// <summary>

    /// Performs an operation on all entities found in this database.

    /// </summary>

    /// <param name="f">Function performing an operation on an entity.</param>

    /// <param name="includeAttribs">Also include attributes.</param>

    /// <returns>The number of times the function returned true.</returns>

 

    public static int ForEachEntity(

      this Database db, Func<Entity, bool> f, bool includeAttribs = false

    )

    {

      int count = 0;

      using (var tr = db.TransactionManager.StartTransaction())

      {

        var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);

        foreach (ObjectId btrId in bt)

        {

          var btr = (BlockTableRecord)tr.GetObject(btrId, OpenMode.ForRead);

          foreach (ObjectId entId in btr)

          {

            var ent = tr.GetObject(entId, OpenMode.ForRead) as Entity;

            if (ent != null && f(ent))

            {

              count++;

            }

            if (includeAttribs)

            {

              var br = ent as BlockReference;

              if (br != null)

              {

                foreach (ObjectId arId in br.AttributeCollection)

                {

                  var ar =

                    tr.GetObject(arId, OpenMode.ForRead)

                    as AttributeReference;

                  if (ar != null && f(ar))

                  {

                    count++;

                  }

                }

              }

            }

          }

        }

        tr.Commit();

      }

      return count;

    }

 

    public static void TouchAllEntities(this Database db, bool incAttbs = false)

    {

      // Invalidating the views doesn't help: we touch all the entities to

      // make sure things get updated

 

      db.ForEachEntity(

        e =>

        {

          try

          {

            e.UpgradeOpen();

            e.RecordGraphicsModified(true);

            e.DowngradeOpen();

            return true;

          }

          catch { }

          return false;

        },

        incAttbs

      );

    }

  }

 

  // A class that derives from WorldDraw and encapsulates ViewportDraw

  // (so we can capture and use WorldDraw geometry calls from V
iewportDraw)

 

  public class PassthroughDraw : WorldDraw

  {

    private PassthroughGeometry _pg;

    private ViewportDraw _vd;

    private bool _forceShaded = false;

 

    // Constructor

 

    public PassthroughDraw(ViewportDraw vd, bool forceShaded = false)

    {

      _vd = vd;

      _forceShaded = forceShaded;

      _pg = new PassthroughGeometry(vd.Geometry);

    }

 

    // WorldDraw protocol

 

    public override WorldGeometry Geometry { get { return _pg; } }

 

    // CommonDraw protocol

 

    public override Context Context { get { return _vd.Context; } }

    public override bool IsDragging { get { return _vd.IsDragging; } }

    public override int NumberOfIsolines

    {

      get { return _vd.NumberOfIsolines; }

    }

    public override Geometry RawGeometry { get { return _vd.RawGeometry; } }

    public override bool RegenAbort { get { return _vd.RegenAbort; } }

    public override RegenType RegenType

    {

      get { return _forceShaded ? RegenType.ShadedDisplay : _vd.RegenType; }

    }

    public override SubEntityTraits SubEntityTraits

    {

      get { return _vd.SubEntityTraits; }

    }

    public override double Deviation(

      DeviationType deviationType, Point3d pointOnCurve

    )

    {

      return _vd.Deviation(deviationType, pointOnCurve);

    }

  }

 

  // A class that derives from WorldGeometry and encapsulates ViewportGeometry

  // (so we can capture and use WorldDraw geometry calls from ViewportDraw)

 

  public class PassthroughGeometry : WorldGeometry

  {

    private ViewportGeometry _vg = null;

 

    // Constructor

 

    public PassthroughGeometry(ViewportGeometry vg)

    {

      _vg = vg;

    }

 

    // WorldGeometry protocol

 

    public override void SetExtents(Extents3d extents) { }

    public override void StartAttributesSegment() { }

 

    // Geometry protocol

 

    public override Matrix3d ModelToWorldTransform

    {

      get { return _vg.ModelToWorldTransform; }

    }

 
;

    public override Matrix3d WorldToModelTransform

    {

      get { return _vg.WorldToModelTransform; }

    }

 

    public override bool Circle(Point3d center, double radius, Vector3d normal)

    {

      return _vg.Circle(center, radius, normal);

    }

 

    public override bool Circle(

      Point3d firstPoint, Point3d secondPoint, Point3d thirdPoint

    )

    {

      return _vg.Circle(firstPoint, secondPoint, thirdPoint);

    }

 

    public override bool CircularArc(

      Point3d start, Point3d point, Point3d endingPoint, ArcType arcType

    )

    {

      return _vg.CircularArc(start, point, endingPoint, arcType);

    }

 

    public override bool CircularArc(

      Point3d center, double radius, Vector3d normal, Vector3d startVector,

      double sweepAngle, ArcType arcType

    )

    {

      return _vg.CircularArc(

        center, radius, normal, startVector, sweepAngle, arcType

      );

    }

 

    public override bool Draw(Drawable value)

    {

      return _vg.Draw(value);

    }

 

    public override bool Edge(Curve2dCollection e)

    {

      return _vg.Edge(e);

    }

 

    public override bool EllipticalArc(

      Point3d center, Vector3d normal, double majorAxisLength,

      double minorAxisLength, double startDegreeInRads, double endDegreeInRads,

      double tiltDegreeInRads, ArcType arcType

    )

    {

      return

        _vg.EllipticalArc(

          center, normal, majorAxisLength, minorAxisLength, startDegreeInRads,

          endDegreeInRads, tiltDegreeInRads, arcType

        );

    }

 

    public override bool Image(

      ImageBGRA32 imageSource, Point3d position, Vector3d u, Vector3d v

    )

    {

      return _vg.Image(imageSource, position, u, v);

    }

 

    public override bool Image(

      ImageBGRA32 imageSource, Point3d position, Vector3d u, Vector3d v,

      TransparencyMode transparencyMode

    )

    {

      return _vg.Image(imageSource, position, u, v, transparencyMode);

    }

 

    public override bool Mesh(

      int rows, int columns, Point3dCollection points, EdgeData edgeData,

      FaceData faceData, VertexData vertexData, bool bAutoGenerateNormals

    )

    {

      return

        _vg.Mesh(

          rows, columns, points, edgeData, faceData, vertexData,

          bAutoGenerateNormals

        );

    }

 

    public override bool OwnerDraw(

      GdiDrawObject gdiDrawObject, Point3d position, Vector3d u, Vector3d v

    )

    {

      return _vg.OwnerDraw(gdiDrawObject, position, u, v);

    }

 

    public override bool Polygon(Point3dCollection points)

    {

      return _vg.Polygon(points);

    }

 

    public override bool Polyline(AcGi.Polyline polylineObj)

    {

      return _vg.Polyline(polylineObj);

    }

 

    public override bool Polyline(

      AcDb.Polyline value, int fromIndex, int segments

    )

    {

      return _vg.Polyline(value, fromIndex, segments);

    }

 

    public override bool Polyline(

      Point3dCollection points, Vector3d normal, IntPtr subEntityMarker

    )

    {

      return _vg.Polyline(points, normal, subEntityMarker);

    }

 

    public override bool Polypoint(

      Point3dCollection points, Vector3dCollection normals,

      IntPtrCollection subentityMarkers

    )

    {

      return _vg.Polypoint(points, normals, subentityMarkers);

    }

 

    public override bool PolyPolygon(

      UInt32Collection numPolygonPositions, Point3dCollection polygonPositions,

      UInt32Collection numPolygonPoints, Point3dCollection polygonPoints,

      EntityColorCollection outlineColors, LinetypeCollection outlineTypes,

      EntityColorCollection fillColors, TransparencyCollection fillOpacities

    )

    {

      return _vg.PolyPolygon(

        numPolygonPoints, polygonPoints, numPolygonPoints, polygonPoints,

        outlineColors, outlineTypes, fillColors, fillOpacities

      );

    }

 

    public override bool PolyPolyline(PolylineCollection polylineCollection)

    {

      return _vg.PolyPolyline(polylineCollection);

    }

 

    public override void PopClipBoundary()

    {

      _vg.PopClipBoundary();

    }

 

    public override bool PopModelTransform()

    {

      return _vg.PopModelTransform();

    }

 

    public override bool PushClipBoundary(ClipBoundary boundary)

    {

      return _vg.PushClipBoundary(boundary);

    }

 

    public override bool PushModelTransform(Matrix3d matrix)

    {

      return _vg.PushModelTransform(matrix);

    }

 

    public override bool PushModelTransform(Vector3d normal)

    {

      return _vg.PushModelTransform(normal);

    }

 

    public override Matrix3d PushOrientationTransform(

      OrientationBehavior behavior

    )

    {

      return _vg.PushOrientationTransform(behavior);

    }

 

    public override Matrix3d PushPositionTransform(

      PositionBehavior behavior, Point2d offset

    )

    {

      return _vg.PushPositionTransform(behavior, offset);

    }

 

    public override Matrix3d PushPositionTransform(

      PositionBehavior behavior, Point3d offset

    )

    {

      return _vg.PushPositionTransform(behavior, offset);

    }

 

    public override Matrix3d PushScaleTransform(

      ScaleBehavior behavior, Point2d extents

    )

    {

      return _vg.PushScaleTransform(behavior, extents);

    }

 

    public override Matrix3d PushScaleTransform(

      ScaleBehavior behavior, Point3d extents

    )

    {

      return _vg.PushScaleTransform(behavior, extents);

    }

 

    public override bool Ray(Point3d point1, Point3d point2)

    {

      return _vg.Ray(point1, point2);

    }

 

    public override bool RowOfDots(int count, Point3d start, Vector3d step)

    {

      return _vg.RowOfDots(count, start, step);

    }

 

    public override bool Shell(

      Point3dCollection points, IntegerCollection faces, EdgeData edgeData,

      FaceData faceData, VertexData vertexData, bool bAutoGenerateNormals

    )

    {

      // To avoid a null argument exception we need to transfer the faces

      // across to a new collection

 

      var faces2 = new IntegerCollection(faces.Count);

      foreach (int face in faces) faces2.Add(face);

      return

        _vg.Shell(

          points, faces2, edgeData, faceData, vertexData, bAutoGenerateNormals

  &#
160;     );

    }

 

    public override bool Text(

      Point3d position, Vector3d normal, Vector3d direction, string message,

      bool raw, TextStyle textStyle

    )

    {

      return _vg.Text(position, normal, direction, message, raw, textStyle);

    }

 

    public override bool Text(

      Point3d position, Vector3d normal, Vector3d direction, double height,

      double width, double oblique, string message

    )

    {

      return

        _vg.Text(position, normal, direction, height, width, oblique, message);

    }

 

    public override bool WorldLine(Point3d startPoint, Point3d endPoint)

    {

      return _vg.WorldLine(startPoint, endPoint);

    }

 

    public override bool Xline(Point3d point1, Point3d point2)

    {

      return _vg.Xline(point1, point2);

    }

  }

 

  public class PerViewportEntityDisplayOverrule : DrawableOverrule

  {

    // We currently have a few issues to work around: solid geometry (including

    // surfaces) doesn't have ViewportDraw called with the right RegenType.

    // We need to force the first ViewportDraw call (of two, usually) to have

    // RegenType of ShadedDisplay. This should only be an issue with 2016 and

    // earlier.

 

    // This collection tracks the ObjectIds of entities we've seen

    // (we effectively toggle the "seen" flag on and off per entity)

 

    private ObjectIdCollection _seen = new ObjectIdCollection();

 

    // We also need to track which viewports are 2D vs. 3D

 

    private Dictionary<int, bool> _is3D = new Dictionary<int, bool>();

 

    // We'll return the same attributes from both SetAttributes and

    // ViewportDrawLogicalFlags

 

    private int GetAttributes(Drawable d, int flags)

    {

      // All entities need to be per-viewport

 

      if (d is Entity)

      {

        flags |=

          (int)(

            DrawableAttributes.IsAnEntity |

            DrawableAttributes.RegenTypeDependentGeometry |

            DrawableAttributes.ViewDependentViewportDraw

          );

      }

 

      if (d is BlockReference)

      {

        // Block references shouldn't have their compound flag set if

        // containing attributes

 

        if ((flags & (int)DrawableAttributes.IsCompoundObject) != 0)

        {

          flags ^= (int)DrawableAttributes.IsCompoundObject;

        }

 

        var br = (BlockReference)d;

        if (br.AttributeCollection.Count > 0)

        {

          flags |= (int)DrawableAttributes.HasAttributes;

        }

      }

      else if (d is BlockTableRecord)

      {

        // Block definitions are drawn per-viewport and may have attributes

 

        flags |=

          (int)(

            DrawableAttributes.RegenTypeDependentGeometry |

            DrawableAttributes.ViewDependentViewportDraw

          );

        var btr = (BlockTableRecord)d;

        if (btr.HasAttributeDefinitions)

        {

          flags |= (int)DrawableAttributes.HasAttributes;

        }

      }

 

      return flags;

    }

 

    public override int SetAttributes(Drawable d, DrawableTraits t)

    {

      return GetAttributes(d, base.SetAttributes(d, t));

    }

 

    public override int ViewportDrawLogicalFlags(Drawable d, ViewportDraw vd)

    {

      return GetAttributes(d, base.ViewportDrawLogicalFlags(d, vd));

    }

 

    // This is where you implement your logic to decide which objects to

    // display. You could tag with XData or maintain your own in-memory

    // structure indicating which entities belong where. We're going to

    // decide it based purely on the type of the object, for simplicity

 

    private bool IsVisible(Drawable d, int vp)

    {

      if (vp == 2)

        return d is Curve;

      if (vp == 3)

        return d is Solid3d;

      if (vp == 4)

        return d is Solid3d || d is AcDb.Surface;

      if (vp == 5)

        return true;

 

      return false;

    }

 

    // We periodically need to clear and regenerate the "seen" and "is 3D"

    // information, as it can change

 

    public void ResetShading(Document doc)

    {

      try

      {

        _seen.Clear();

        PopulateVpData(doc);

      }

      catch { }

    }

 

    // Refresh our cache of per-viewport "is 3D" data

 

    private void PopulateVpData(Document doc)

    {

      var gm = doc.GraphicsManager;

      var db = doc.Database;

 

      // Do we need to regen at the end?

 

      var needRegen = false;

 

      // Use an open/close transaction

 

      using (var tr = db.TransactionManager.StartOpenCloseTransaction())

      {

        var vt =

          (ViewportTable)tr.GetObject(db.ViewportTableId, OpenMode.ForRead);

 

        foreach (ObjectId vtrId in vt)

        {

          var isVp3D = false;

          var vpNeedsRegen = false;

 

          var vtr =

            (ViewportTableRecord)tr.GetObject(vtrId, OpenMode.ForRead);

 

          // Check the viewport's visual style

 

          if (vtr.VisualStyleId != ObjectId.Null)

          {

            // As an added feature we're making 3D Wireframe look 2D, too

 

            var vs =

              (DBVisualStyle)tr.GetObject(vtr.VisualStyleId, OpenMode.ForRead);

            isVp3D =

              (vs.Type != VisualStyleType.Wireframe2D &&

               vs.Type != VisualStyleType.Wireframe3D);

          }

 

          // Only update the data if it has changed: this way we only regen

          // when needed

 

          if (_is3D.ContainsKey(vtr.Number))

          {

            if (_is3D[vtr.Number] != isVp3D)

            {

              // If the value for this viewport has changed, we're going

              // to invalidate the GS cache and force a regen

 

              _is3D[vtr.Number] = isVp3D;

              vpNeedsRegen = true;

            }

          }

          else

          {

            _is3D.Add(vtr.Number, isVp3D);

          }

 

          if (vpNeedsRegen)

          {

            // Invalidate the vieport's GS cache and trigger a full regen

 

            var v = gm.GetCurrent3dAcGsView(vtr.Number);

            if (v != null)

              v.InvalidateCachedViewportGeometry();

            needRegen = true;

          }

        }

 
0;      tr.Commit();

      }

 

      // Now we regen, if needed

 

      if (needRegen)

        doc.Editor.Regen();

    }

 

    public bool Is3D(int vp)

    {

      return _is3D.ContainsKey(vp) ? _is3D[vp] : false;

    }

 

    public override bool WorldDraw(Drawable d, WorldDraw wd)

    {

      // Defer all calls to ViewportDraw

 

      return false;

    }

 

    public override void ViewportDraw(Drawable d, ViewportDraw vd)

    {

      // There's an issue with viewportDraw for solids/surfaces...

      // It should be called twice, once with ShadedDisplay and then

      // once with StandardDisplay. In AutoCAD 2016 and before it gets

      // called twice with StandardDisplay. So we tell the passthrough

      // object to force to ShadedDisplay the first time a solid or

      // surface gets drawn. All being well.

 

      var forceShaded = false;

 

      // Do we have a solid or a surface in a 3D viewport?

 

      var in3D = Is3D(vd.Viewport.AcadWindowId);

      if (in3D && (d is Solid3d || d is AcDb.Surface))

      {

        // Check whether we have its ID in our "seen" list

 

        var e = (Entity)d;

        if (_seen.Contains(e.ObjectId))

        {

          // If yes, remove it from the list

 

          _seen.Remove(e.ObjectId);

        }

        else

        {

          // If no, force to ShadedDisplay (and add the object, so

          // the next call won't be shaded)

 

          forceShaded = true;

          _seen.Add(e.ObjectId);

        }

      }

 

      // We shouldn't be getting exceptions here under normal circumstances...

      // You will want to add soem kind of logging or output for when something

      // bad happens

 

      try

      {

        // Check whether the object is visible in this viewport

 

        if (IsVisible(d, vd.Viewport.AcadWindowId))

        {

          // If so, create our passthrough object (which will take WorldDraw

          // calls and pass them through to our ViewportDraw object)

 

          var pd = new PassthroughDraw(vd, forceShaded);

 

          // If we're dealing with a block or an attribute definition -

          // or we can't use WorldDraw in this scenario with our passthrough

          // object - then go ahead and call ViewportDraw, directly

 

          if (

            d is BlockReference ||

            d is AttributeDefinition ||

            d is Light ||

            !base.WorldDraw(d, pd)

          )

          {

            base.ViewportDraw(d, vd);

          }

        }

      }

      catch {}

    }

  }

 

  public class Commands

  {

    // Shared member variable to store our Overrule instance

 

    private static PerViewportEntityDisplayOverrule _orule;

 

    [CommandMethod("PVE")]

    public static void ToggleOverrule()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

      var db = doc.Database;

 

      // Initialize overrule if it doesn't exist

 

      if (_orule == null)

      {

        // Turn overruling on

 

        _orule = new PerViewportEntityDisplayOverrule();

        Overrule.AddOverrule(RXObject.GetClass(typeof(DBObject)), _orule, false);

        Overrule.Overruling = true;

 

        _orule.ResetShading(doc);

      }

      else

      {

        // Remove the overrule

 

        Overrule.RemoveOverrule(RXObject.GetClass(typeof(DBObject)), _orule);

        _orule.Dispose();

        _orule = null;

 

        doc.Editor.Command("_.REGENALL");

      }

 

      db.TouchAllEntities(true);

 

      // This is a kludge: for the current document, reset the shading tracking

      // at the end of each command. The tracking will hopefully not be

      // needed post 2016

 

      doc.CommandWillStart += (s, e) => { _orule.ResetShading((Document)s); };

      doc.CommandEnded += (s, e) => { _orule.ResetShading((Document)s); };

    }

  }

}

 

I wrote the code in C# but the technique certainly remains valid for native ObjectARX, too (in fact using C++ would almost certainly avoid the Shell() marshalling problem).

As you can see from the code and the introduction to this post, there were quite a few issues to work through to get it working. It's also very possible I've missed some. I'd certainly appreciate feedback as people try the code out and work through scenarios I haven't tested, as I'm very keen to get a solid, general solution to this problem in place.

Update:

I made a few minor adjustments to better cope with entities on locked layers and not crash when dealing with Light objects. I expect more of these cases to crop up… please keep them coming!

5 responses to “Displaying different entities in AutoCAD viewports using .NET”

  1. Thomas Brammer (ich persönlich Avatar
    Thomas Brammer (ich persönlich

    It would be great if AutoCAD had build-in support for per-viewport-visibility of ENTITIES just as it has for per-viewport-visibility of LAYERS. This is on top of my wishlist since years!
    However: Well done, Kean.

  2. I'm drawing custom MTEXT annotations in ViewportDraw with custom rotations. If I have multiple viewports with different UCSs then they INDIVIDUALLY draw the annotations with correct rotation. But in the case of a REGENALL only the active viewport is correct. I've verified the routine is correctly getting the same correct ucs rotation for each viewport.

    1. Kean Walmsley Avatar

      I suggest posting a reproducible sample to the AutoCAD .NET forum...

      Kean

      1. Got it, in case of regenall, in the model tab, the current viewport CVPORT is always drawn first, so you can calculate its ucs rotation and save the value, the correct MTEXT.rotation value is a combination of the desired rotation, the ucs rotation of the viewport being drawn and the saved rotation. In a Layout tab only the ucs rotation of the viewport is needed plus a little math.

        1. Kean Walmsley Avatar

          Great!

          Kean

Leave a Reply to JVic Cancel reply

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