Wednesday, June 21, 2023

Auto-Rotating Annotating Entities in Viewport with DrawableOverrule

When annotating entities (Text, MText, or Block with Attributes) are used in ModelSpace as if they are labels, they often are made horizonal in terms of their textual presentation (or all with certain rotation, for that matter). However, CAD users would face an annoying issue, if the drawing contents have to be presented in various layout viewports, which may have different twist angles, depending how the drawing contents in the ModelSpace is to be presented in the viewport. CAD users may have to spent time to rotate the annotating entities that visible in a viewport, so that they appear in the viewport as horizontal (or vertical). 

In the past, I had written custom command at my work to make label-like entities being rotated to horizontal/vertical based on which viewport they appeared. While this worked OK in many cases, but it would obviously not work well, if an annotating entity was visible in multiple viewports, which may have different twist angle.

AutoCAD Civil 3D users know that C3D labels automatically appear horizontally in layout viewports, regardless their twist angles. So, can we achieve the same or similar effect with plain AutoCAD (e.g. making annotating entities - DBText, MText, BlockReference with Attributes - to appear always horizontally in layout viewports)? The answer is "yes". This article demonstrates how to use DrawableOrverule to do it.

A custom DrawableOverrule overrides either its WorldDraw(), or ViewportDraw(), or both, to make the target entity visually different from its built-in appearance. In most cases overridden WorldDraw() is used. For example, we can make a curve (Line, Polyline...) looks like a 3D pipe, a DBPoint looks like a ball. Because of the topic of this article, while the main focus is to override ViewportDraw() method, so that the annotating entities would be appeared horizontally in layout viewports, the WorldDraw() must(!) also be overridden (I placed comments in the code). The code is following.

Class LabelDrawableOverrule:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.Runtime;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace LabelTextDrawableOverrule
{
    public class LabelDrawableOverrule : DrawableOverrule
    {
        private const string BLOCK_NAME = "TESTLABEL";
        private static LabelDrawableOverrule _instance;
        private bool _isEnabled = false;
        private bool _originalOverruling = false;
 
        #region public properties
        public static LabelDrawableOverrule Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new LabelDrawableOverrule();
                }
                return _instance;
            }
        }
 
        public bool IsEnabled=>_isEnabled;
 
        #endregion
 
        public void RunOverrule()
        {
            if (!_isEnabled)
            {
                EnableOverrule();
            }
            else
            {
                DisableOverrule();
            }
            Application.DocumentManager.MdiActiveDocument.Editor.Regen();
        }
 
        public override bool IsApplicable(RXObject overruledSubject)
        {
            var blk = overruledSubject as BlockReference;
            if (blk!=null && blk.Name.ToUpper()==BLOCK_NAME)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
 
        /// <summary>
        /// It is important to override WorldDraw() method and
        /// bypass calling base.WorldDraw() and return false
        /// </summary>
        /// <param name="drawable"></param>
        /// <param name="wd"></param>
        /// <returns></returns>
        public override bool WorldDraw(Drawable drawable, WorldDraw wd)
        {
            //base.WorldDraw(drawable, wd);
            return false;
        }
 
        public override void ViewportDraw(Drawable drawable, ViewportDraw vd)
        {
            var ent = drawable as BlockReference; // because this Overrule targets BlockReference
            if (ent == nullreturn;
 
            if (vd.ViewportObjectId.IsNull)
            {
                // Rotating overruled entity in ModelSpace to horizontal
                if (ent.Rotation != 0.0)
                {
                    using (var clone = ent.Clone() as BlockReference)
                    {
                        clone.TransformBy(
                            Matrix3d.Rotation(-ent.Rotation, Vector3d.ZAxis, ent.Position));
                        base.ViewportDraw(clone, vd);
                        return;
                    }
                }
            }
            else
            {
                if (IsLabelBlockVisibleFromViewport(vd.ViewportObjectId, ent, out double tweestAngle))
                {
                    using (var clone = ent.Clone() as BlockReference)
                    {
                        clone.TransformBy(Matrix3d.Rotation(
                            -(tweestAngle + ent.Rotation), Vector3d.ZAxis, ent.Position));
                        base.ViewportDraw(clone, vd);
                        return;
                    }
                }
            }
 
            base.ViewportDraw(drawable, vd);
        }
 
        #region private methods
 
        private void EnableOverrule()
        {
            _originalOverruling = Overrule.Overruling;
            AddOverrule(RXClass.GetClass(typeof(BlockReference)), thisfalse);
            SetCustomFilter();
            _isEnabled = true;
        }
 
        private void DisableOverrule()
        {
            RemoveOverrule(RXClass.GetClass(typeof(BlockReference)), this);
            _isEnabled = false;
            Overrule.Overruling=_originalOverruling;
        }
 
        // for simplicity, I consider the label block is visible in the viewport
        // if its insertion point is inside the viewport boundary projected to ModelSpace
        private bool IsLabelBlockVisibleFromViewport(
            ObjectId vportId, BlockReference labelBlockout double angle)
        {
            angle = 0.0;
            var isInside = false;
 
            using (var tran=vportId.Database.TransactionManager.StartOpenCloseTransaction())
            {
                var vport = (CadDb.Viewport)tran.GetObject(vportId, OpenMode.ForRead);
                angle = vport.TwistAngle;
 
                using (var vportBoundaryInModel = GetViewportBoundaryInModelSpace(vport, tran))
                {
                    isInside =
                        CadHelper.IsPointInside(labelBlock.Position, vportBoundaryInModel);
                }
 
                tran.Commit();
            }
 
            return isInside;
        }
 
        private CadDb.Polyline GetViewportBoundaryInModelSpace(
            CadDb.Viewport vport, Transaction tran)
        {
            CadDb.Polyline poly;
            var mt = CadHelper.PaperToModel(vport);
 
            if (!vport.NonRectClipEntityId.IsNull && vport.NonRectClipOn)
            {
                var clipPoly = (CadDb.Polyline)tran.GetObject(
                    vport.NonRectClipEntityId, OpenMode.ForRead);
                poly = clipPoly.Clone() as CadDb.Polyline;
            }
            else
            {
                poly = GetViewportBoundary(vport, mt);
            }
 
            return poly;
        }
 
        private CadDb.Polyline GetViewportBoundary(
            CadDb.Viewport vport, Matrix3d paperToModelTransform)
        {
            CadDb.Polyline poly = new CadDb.Polyline();
    
            Extents3d ext = vport.GeometricExtents;
            Point3d pt;
 
            pt=(new Point3d(ext.MinPoint.X, ext.MinPoint.Y, 0.0)).
                TransformBy(paperToModelTransform);
            poly.AddVertexAt(0, new Point2d(pt.X, pt.Y), 0.0, 0.0, 0.0);
 
            pt = (new Point3d(ext.MinPoint.X, ext.MaxPoint.Y, 0.0)).
                TransformBy(paperToModelTransform);
            poly.AddVertexAt(1, new Point2d(pt.X, pt.Y), 0.0, 0.0, 0.0);
 
            pt = (new Point3d(ext.MaxPoint.X, ext.MaxPoint.Y, 0.0)).
                TransformBy(paperToModelTransform);
            poly.AddVertexAt(2, new Point2d(pt.X, pt.Y), 0.0, 0.0, 0.0);
 
            pt = (new Point3d(ext.MaxPoint.X, ext.MinPoint.Y, 0.0)).
                TransformBy(paperToModelTransform);
            poly.AddVertexAt(3, new Point2d(pt.X, pt.Y), 0.0, 0.0, 0.0);
 
            poly.Closed = true;
            return poly;
        }
        
        private CadDb.Polyline GetNonRectClipBoundary(
            CadDb.Polyline polyline, Matrix3d paperToModelTransform)
        {
            Point3dCollection points = new Point3dCollection();
            for (int i = 0; i<polyline.NumberOfVertices; i++)
            {   
                points.Add(polyline.GetPoint3dAt(i).TransformBy(paperToModelTransform));
            }
 
            var poly = new CadDb.Polyline();
            var n = 0;
            foreach (Point3d p in points)
            {
                poly.AddVertexAt(n, new Point2d(p.X, p.Y), 0.0, 0.0, 0.0);
                n++;
            }
            poly.Closed = true;
            return poly;
        }
 
        #endregion
    }
}

Class CadHelper, in which I referenced AcMPolygonMgd.dll for testing if a point is inside of closed polyline:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using System;
 
namespace LabelTextDrawableOverrule
{
    public class CadHelper
    {
 
        #region
        //**********************************************************************
        //Create coordinate transform matrix
        //between modelspace and paperspace viewport
        //The code is borrowed from
        //http://www.theswamp.org/index.php?topic=34590.msg398539#msg398539
        //*********************************************************************
        public static Matrix3d PaperToModel(Viewport vp)
        {
            Matrix3d mx = ModelToPaper(vp);
            return mx.Inverse();
        }
   
        public static Matrix3d ModelToPaper(Viewport vp)
        {
            Vector3d vd = vp.ViewDirection;
            Point3d vc = new Point3d(vp.ViewCenter.X, vp.ViewCenter.Y, 0);
            Point3d vt = vp.ViewTarget;
            Point3d cp = vp.CenterPoint;
            double ta = -vp.TwistAngle;
            double vh = vp.ViewHeight;
            double height = vp.Height;
            double width = vp.Width;
            double scale = vh / height;
            double lensLength = vp.LensLength;
            Vector3d zaxis = vd.GetNormal();
            Vector3d xaxis = Vector3d.ZAxis.CrossProduct(vd);
            Vector3d yaxis;
   
            if (!xaxis.IsZeroLength())
            {
                xaxis = xaxis.GetNormal();
                yaxis = zaxis.CrossProduct(xaxis);
            }
            else if (zaxis.Z < 0)
            {
                xaxis = Vector3d.XAxis * -1;
                yaxis = Vector3d.YAxis;
                zaxis = Vector3d.ZAxis * -1;
            }
            else
            {
                xaxis = Vector3d.XAxis;
                yaxis = Vector3d.YAxis;
                zaxis = Vector3d.ZAxis;
            }
            Matrix3d pcsToDCS = Matrix3d.Displacement(Point3d.Origin - cp);
            pcsToDCS = pcsToDCS * Matrix3d.Scaling(scale, cp);
            Matrix3d dcsToWcs = Matrix3d.Displacement(vc - Point3d.Origin);
            Matrix3d mxCoords = Matrix3d.AlignCoordinateSystem(
                Point3d.Origin, Vector3d.XAxis, Vector3d.YAxis,
                Vector3d.ZAxis, Point3d.Origin,
                xaxis, yaxis, zaxis);
            dcsToWcs = mxCoords * dcsToWcs;
            dcsToWcs = Matrix3d.Displacement(vt - Point3d.Origin) * dcsToWcs;
            dcsToWcs = Matrix3d.Rotation(ta, zaxis, vt) * dcsToWcs;
 
            Matrix3d perspectiveMx = Matrix3d.Identity;
            if (vp.PerspectiveOn)
            {
                double vSize = vh;
                double aspectRatio = width / height;
                double adjustFactor = 1.0 / 42.0;
                double adjstLenLgth = vSize * lensLength *
                    Math.Sqrt(1.0 + aspectRatio * aspectRatio) * adjustFactor;
                double iDist = vd.Length;
                double lensDist = iDist - adjstLenLgth;
                double[] dataAry = new double[]
                {
                    1,0,0,0,0,1,0,0,0,0,
                    (adjstLenLgth - lensDist) / adjstLenLgth,
                    lensDist * (iDist - adjstLenLgth) / adjstLenLgth,
                    0,0,-1.0 / adjstLenLgth,iDist / adjstLenLgth
                };
    
                perspectiveMx = new Matrix3d(dataAry);
            }
 
            Matrix3d finalMx =
                pcsToDCS.Inverse() *perspectiveMx * dcsToWcs.Inverse();
 
            return finalMx;
        }
 
        public static bool IsPointInside(Point3d point, Polyline boundarybool onEdgeAsInside=true)
        {
 
            using (MPolygon mPolygon = MakeMPolygon(boundary))
            {
                for (int i = 0; i < mPolygon.NumMPolygonLoops; i++)
                {
                    if (mPolygon.IsPointOnLoopBoundary(point, i, Tolerance.Global.EqualPoint))
                    {
                        if (onEdgeAsInside) return true;
                    }
                }
 
                if (mPolygon.IsPointInsideMPolygon(point, Tolerance.Global.EqualPoint).Count == 1)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }
 
        #endregion
 
        #region private methods
 
        private static MPolygon MakeMPolygon(Polyline pline)
        {
            MPolygon mPolygon = new MPolygon();
            mPolygon.AppendLoopFromBoundary(pline, false, Tolerance.Global.EqualPoint);
            mPolygon.Elevation = pline.Elevation;
            mPolygon.Normal = pline.Normal;
            return mPolygon;
        }
 
        #endregion
    }
}

And finally, the CommandClass:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(LabelTextDrawableOverrule.MyCommands))]
 
namespace LabelTextDrawableOverrule
{
    public class MyCommands
    {
        [CommandMethod("LabelOverrule")]
        public static void RunLabelOverrule()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            LabelDrawableOverrule.Instance.RunOverrule();
            var msg = LabelDrawableOverrule.Instance.IsEnabled ?
                "LabelDrawableOverrule is enabled." : 
                "LabelDrawableOverrule is disabled.";
            ed.WriteMessage($"\n{msg}\n");
        }
    }
}

Points of interest:

  1. The ViewportDraw argument of the ViewportDraw() method provided ObjectId of the layout viewport, so that we can use to find out the Viewport.TwistAngle and also find out if the overruled entity is visible in the viewport. In my code here, for simplicity, I assumed the label block entity is visible in the viewport if its insertion point is inside of the viewport boundary projected in the ModelSpace.
  2. We'll need to make sure WorldDraw() method is overridden and do not call base.WorldDraw() inside. We also must let it return false, so that the ViewportDraw() method is called by the Overrule.
  3. I used a copy of the overruled entity and rotated it as needed and pass it to base.ViewportDraw() method. That is, the overruled entity itself remained unchanged (e.g. the block's Rotation property was unchanged), only its image in the viewport got rotated (to be horizonal). I could have rotated the overruled entity, but if the entities is visible in multiple viewports with different twist angles, it would end up with constant changing rotations.
See the video clip below showing the code running result:



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.