Showing posts with label AutoCAD. Show all posts
Showing posts with label AutoCAD. Show all posts

Sunday, November 29, 2020

Find Line's Intersection Points With Block (BlockReference)

 I recently replied a question posted in AutoCAD .NET discussion forum, in which I proposed a code workflow. Then another question in similar context came up. So I thought I might as well write some code to demonstrate my idea in the reply to the first question, which would indirectly answer the second question: after all, once the intersection points of line and the block reference are known, trimming the line would be simple next step.

Firstly to simplify the case, I limit the discussion only on Line and BlockReference with nested entities being Curve type only.

As we know all classes derived from Entity have overloaded method IntersectWith(), which can be used to find intersection points between 2 entities. BlockReference, being derived from Entity, inherently also has its IntersectWith() method. However, because BlockReference is a reference of a composite object with many different entities nested, its IntersectWtith() method is implemented in its own way: it uses its bounding box (GeometricExtents) as its boundary to calculate its intersection points with other entity. Following code demonstrate this:

#region Command to find entity's intersecting point with block: block's bounding box
 
[CommandMethod("BlkIntersect1")]
public static void TestBlockIntersection1()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var res = ed.GetEntity("\nSelect block:");
    if (res.Status == PromptStatus.OK)
    {
        if (res.ObjectId.ObjectClass.DxfName.ToUpper() != "INSERT")
        {
            ed.WriteMessage("\nNot a block!");
            return;
        }
 
        dwg.Database.Pdmode = 34;
 
        try
        {
            CadHelper.Highlight(res.ObjectId, true);
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var blk = (BlockReference)tran.GetObject(
                    res.ObjectId, OpenMode.ForRead);
                var space = (BlockTableRecord)tran.GetObject(
                    dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
 
                // create a rectangle polyline to show the block's GeometricExtents
                var boundBox = CreateBoundBox(blk);
                space.AppendEntity(boundBox);
                tran.AddNewlyCreatedDBObject(boundBox, true);
                tran.TransactionManager.QueueForGraphicsFlush();
 
                while (true)
                {
                    var eRes = ed.GetEntity(
                        "\nSelect an entity intersecting the block:");
                    if (eRes.Status == PromptStatus.OK)
                    {
                        FindBlockIntersectionPoint(eRes.ObjectId, blk, space, tran);
                    }
                    else
                    {
                        break;
                    }
                }
 
                tran.Commit();
            }
        }
        finally
        {
            CadHelper.Highlight(res.ObjectId, false);
        }
    }
}
 
private static void FindBlockIntersectionPoint(
    ObjectId entId, BlockReference blk, BlockTableRecord space, Transaction tran)
{
 
    var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
 
    var pts = new Point3dCollection();
    blk.IntersectWith(ent, Intersect.OnBothOperands, pts, IntPtr.Zero, IntPtr.Zero);
 
    if (pts.Count > 0)
    {
        foreach (Point3d pt in pts)
        {
            var dbPt = new DBPoint(pt);
            space.AppendEntity(dbPt);
            tran.AddNewlyCreatedDBObject(dbPt, true);
            tran.TransactionManager.QueueForGraphicsFlush();
        }
    }
 
}
 
private static Polyline CreateBoundBox(BlockReference blk)
{
    var ext = blk.GeometricExtents;
    var poly = new Polyline(4);
    poly.AddVertexAt(
        0, new Point2d(ext.MinPoint.X, ext.MinPoint.Y),
        0.0, 0.0, 0.0);
    poly.AddVertexAt(
        0, new Point2d(ext.MinPoint.X, ext.MaxPoint.Y),
        0.0, 0.0, 0.0);
    poly.AddVertexAt(
        0, new Point2d(ext.MaxPoint.X, ext.MaxPoint.Y),
        0.0, 0.0, 0.0);
    poly.AddVertexAt(
        0, new Point2d(ext.MaxPoint.X, ext.MinPoint.Y),
        0.0, 0.0, 0.0);
    poly.Closed = true;
    poly.ColorIndex = 2;
    return poly;
}
 
#endregion

The code draws a rectangle as the block's bounding box and draws point at intersecting point of a line and the block reference. As following video shows the intersection point is at the location where the line and the bounding box intersect to each other.


Now here is the code I proposed to the first and/or second question: find actual intersection point of a line to the block's "real" boundary (outmost entity nested in the block).

#region command to find entity's intersecting point: block's real boundary
 
[CommandMethod("BlkIntersect2")]
public static void TestBlockIntersection2()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var res = ed.GetEntity("\nSelect block:");
    if (res.Status == PromptStatus.OK)
    {
        if (res.ObjectId.ObjectClass.DxfName.ToUpper() != "INSERT")
        {
            ed.WriteMessage("\nNot a block!");
            return;
        }
 
        dwg.Database.Pdmode = 34;
 
        try
        {
            CadHelper.Highlight(res.ObjectId, true);
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var blk = (BlockReference)tran.GetObject(
                    res.ObjectId, OpenMode.ForRead);
                var space = (BlockTableRecord)tran.GetObject(
                    dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
 
                while (true)
                {
                    var opt = new PromptEntityOptions(
                        "\nSelect a line intersecting with the block:");
                    opt.SetRejectMessage("\nInvalid: not a line!");
                    opt.AddAllowedClass(typeof(Line), true);
                    var eRes = ed.GetEntity(opt);
                    if (eRes.Status == PromptStatus.OK)
                    {
                        GetBlockIntersectionPoints(
                            eRes.ObjectId, blk, space, tran);
                    }
                    else
                    {
                        break;
                    }
                }
 
                tran.Commit();
            }
        }
        finally
        {
            CadHelper.Highlight(res.ObjectId, false);
        }
    }
}
 
private static void GetBlockIntersectionPoints(
    ObjectId entId, BlockReference blk, BlockTableRecord space, Transaction tran)
{
    var line = (Line)tran.GetObject(entId, OpenMode.ForRead);
    if (FindOutmostIntersectingPoints(line, blk, out Point3d pt1, out Point3d pt2))
    {
        var dbPt = new DBPoint(pt1);
        space.AppendEntity(dbPt);
        tran.AddNewlyCreatedDBObject(dbPt, true);
        tran.TransactionManager.QueueForGraphicsFlush();
 
        dbPt = new DBPoint(pt2);
        space.AppendEntity(dbPt);
        tran.AddNewlyCreatedDBObject(dbPt, true);
        tran.TransactionManager.QueueForGraphicsFlush();
    }
}
 
private static bool FindOutmostIntersectingPoints(
    Line line, BlockReference blk, out Point3d pt1, out Point3d pt2)
{
    pt1 = Point3d.Origin;
    pt2 = Point3d.Origin;
 
    var points = GetAllIntersectingPoints(line, blk);
    if (points.Count>0)
    {
        pt1 = (from p in points
                orderby p.DistanceTo(line.StartPoint)
                select p).First();
 
        pt2 = (from p in points
                orderby p.DistanceTo(line.EndPoint)
                select p).First();
 
        return true;
    }
    else
    {
        return false;
    }
}
 
private static List<Point3d> GetAllIntersectingPoints(
    Line line, BlockReference blk)
{
    var points = new List<Point3d>();
 
    using (var ents = new DBObjectCollection())
    {
        blk.Explode(ents);
        foreach (DBObject o in ents)
        {
            var ent = (Entity)o;
            var pts = new Point3dCollection();
            line.IntersectWith(
                ent, Intersect.OnBothOperands, pts, IntPtr.Zero, IntPtr.Zero);
            foreach(Point3d p in pts)
            {
                points.Add(p);
            }
            o.Dispose();
        }
    }
 
    return points;
}
 
#endregion

The video below show the code is able to find the 2 intersection points of a line that pass through a block reference, which fall on the block's outmost entity or entities. Obviously this indirectly answers the second question: with these 2 points, the line could ne trimmed easily.


As aforementioned, I limit the code to only apply to Line and block with only Curve type as nested entities. Extra considerations are needed in other cases, such as:

1. If a nested entity in block is a BlockReference, Text/MText/AttributeReference, then the intersection point is on their bounding box. Recursive exploding BlockReference might be needed.

2. If start or end point, or both the intersecting Line/Curve locate inside the block, it might be quite difficult to determine the start/end point is only inside the block's bounding box but outside the outmost entities, or not.

3. If the intersecting entity with block is not a Line, it could intersect with the block more than 2 times.

So writing code to cover all possible cases would be quite some work, if it is possible at all.

Update

As the comment pointed out, I forgot to post the code of Highlight(ObjectId, bool). Here is the code:

public static void Highlight(ObjectId entId, bool highlight)
{
    using (var tran = entId.Database.TransactionManager.StartOpenCloseTransaction())
    {
        var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
        if (highlight)
        {
            ent.Highlight();
        }
        else
        {
            ent.Unhighlight();
        }
        tran.Commit();
    }
}

Wednesday, May 13, 2020

Prevent Certain Properties of an Entity from Being Changed

In the past I wrote about using ObjectOverrule to prevent entities in AutoCAD from being changed/modified and to force entities being changed in certain way.

An interesting question was raised recently in the Autodesk's discussion forum on how to disable geometry editing. To me, the question could have been a more general one: how to make some of properties of an entity not changeable, while other still can be changed? Once again, ObjectOverrule can play well in the game.

Here is the general idea of doing it with ObjectOverrule:

1. Overriding ObjectOverrule.Open() method: if an entity is opened for write, the values of some target properties, which we do not want them to be changed, will be saved, tagged with ObjectId.

2 Overriding ObjectOverrule.Close() method: check whether there is saved original property value for the entity (by its ObjectId), if found, restore the properties with original values.

Based on this idea, I designed a custom ObjectOverrule. Some explanations are following:

1. The Overrule applies to more general type Entity. It can be more specific, such as Curve, Line..., as needed;

2. No filter is set up. But if needed, proper filter would reduce the overhead in AutoCAD caused by Overrule, minor, or significant;

3. Paired Func/Action is injected into the Overrule whenever the Overrule is enabled. The pair of Func/Action plays the role of extracting original property values in Open() method, and restoring property values in Close() method. This way, the actual value extracting/restoring code is separated from the Overrule itself, so programmer can easily write code to decide what properties to target on different type of entities.

Note: It is even possible to abstract the paired Func/Action into an interface, and implement them in separate project, and make them configurable, so that the custom Overrule can be configured to target different properties, different entity types without having to update code at all. But I'll leave this out of this article.

Here are the code.

Class EntityTarget: it has EntityType property and the pair of Func/Action as properties, used to extract target entity's property values and restore them later.

public class EntityTarget
{
    public Type EntityType { setget; }
    public Func<EntityDictionary<stringobject>> PropExtractFunc
    { setget; }
    public Action<EntityDictionary<stringobject>> PropRestoreAction
    { setget; }
}

Class PropertyFreezeOverrule: its has overridden Open<> and Close() method, in which entity's target property values are extracted into a Dictionary, and then saved in Dictionary keyed with ObjectId at Overrule class level; when the entity is closed, the values of entity's target properties will be restored, if necessary. This, target properties of the entity become not changeable.

using System.Collections.Generic;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
 
namespace FreezePropertyOverrule
{
    public class PropertyFreezeOverrule : ObjectOverrule
    {
        private Dictionary<ObjectIdDictionary<stringobject>> _openedEntities = 
            new Dictionary<ObjectIdDictionary<stringobject>>();
        private List<EntityTarget> _targets = new List<EntityTarget>();
 
        private bool _overruling = false;
 
        private bool _started = false;
 
        public void Start(IEnumerable<EntityTargetoverruleTargets)
        {
            if (_started) Stop();
 
            _targets.Clear();
            _targets.AddRange(overruleTargets);
 
            _openedEntities.Clear();
 
            _overruling = Overrule.Overruling;
 
            Overrule.AddOverrule(RXClass.GetClass(typeof(Curve)), thistrue);
            Overruling = true;
            _started = true;
        }
 
        public void Stop()
        {
            Overrule.RemoveOverrule(RXClass.GetClass(typeof(Curve)), this);
            Overrule.Overruling = _overruling;
            _started = false;
        }
 
        public override void Open(DBObject dbObjectOpenMode mode)
        {
            if (mode == OpenMode.ForWrite)
            {
                ExtractTargetProperties(dbObject);
            }
            base.Open(dbObjectmode);
        }
 
        public override void Close(DBObject dbObject)
        {
            RestoreTargetProperties(dbObject);
            base.Close(dbObject);
        }
 
        #region private methods
 
        private void ExtractTargetProperties(DBObject dbObject)
        {
            foreach (var target in _targets)
            {
                if (dbObject.GetRXClass()==RXClass.GetClass(target.EntityType))
                {
                    var propData = target.PropExtractFunc(dbObject as Entity);
                    if (_openedEntities.ContainsKey(dbObject.ObjectId))
                    {
                        _openedEntities[dbObject.ObjectId] = propData;
                    }
                    else
                    {
                        _openedEntities.Add(dbObject.ObjectId, propData);
                    }
                    break;
                }
            }
        }
 
        private void RestoreTargetProperties(DBObject dbObject)
        {
            if (dbObject.IsUndoing) return;
            if (!dbObject.IsModified) return;
            if (dbObject.IsErased) return;
            if (!dbObject.IsWriteEnabled) return;
 
            if (!_openedEntities.ContainsKey(dbObject.ObjectId)) return;
 
            foreach (var target in _targets)
            {
                if (dbObject.GetRXClass() == RXClass.GetClass(target.EntityType))
                {
                    var propData = _openedEntities[dbObject.ObjectId];
                    if (propData!=null)
                    {
                        target.PropRestoreAction(dbObject as EntitypropData);
                        PropertyExtractRestoreUtils.SendMessageToCommandLine(
                            "\nWARNING: editing to this entity was overrule. No change is allowed!\n");
                    }
                    _openedEntities.Remove(dbObject.ObjectId);
                    break;
                }
            }
        }
        #endregion
    }
}

Class PropertyExtracRestoreUtils: it defines a series of paired Func/Action to extracting/restoring entity of specific type. I use the naming convention of GetXxxxProperties() and RestoreXxxxProperties(), where Xxxx is the entity type. For the simplicity, I only defined the pair methods for Line and Circle. But it is easy to add more pairs to target other entity types. Since I also only want to keep Line/Circle geometrically frozen, so, for Line, the properties to freeze are StartPoint and EndPoint; for Circle, Center and Radius.

public static class PropertyExtractRestoreUtils
{
    private const string LINE_START_POINT = "StartPoint";
    private const string LINE_END_POINT = "EndPoint";
 
    private const string CIRCLE_CENTER = "Center";
    private const string CIRCLE_RADIUS = "Radius";
 
    public static Dictionary<stringobjectGetLineProperties(Entity line)
    {
        Dictionary<stringobjectprops = null;
 
        var l = line as Line;
        if (l!=null)
        {
            props = new Dictionary<stringobject>();
            props.Add(LINE_START_POINT, l.StartPoint);
            props.Add(LINE_END_POINT, l.EndPoint);
        }
 
        return props;
    }
 
    public static void RestoreLineProperties(
        Entity lineDictionary<stringobjectproperties)
    {
        var l = line as Line;
        if (l != null)
        {
            if (properties.ContainsKey(LINE_START_POINT) &&
                properties.ContainsKey(LINE_END_POINT))
            {
                l.StartPoint = (Point3d)properties[LINE_START_POINT];
                l.EndPoint = (Point3d)properties[LINE_END_POINT];
            }
        }
    }
 
    public static Dictionary<stringobjectGetCircleProperties(Entity circle)
    {
        Dictionary<stringobjectprops = null;
        var c = circle as Circle;
        if (c!=null)
        {
            props = new Dictionary<stringobject>();
            props.Add(CIRCLE_CENTER, c.Center);
            props.Add(CIRCLE_RADIUS, c.Radius);
        }
 
        return props;
    }
 
    public static void RestoreCircleProperties(
        Entity circleDictionary<stringobjectproperties)
    {
        var c = circle as Circle;
        if (c != null)
        {
            if (properties.ContainsKey(CIRCLE_CENTER) &&
                properties.ContainsKey(CIRCLE_RADIUS))
            {
                c.Center = (Point3d)properties[CIRCLE_CENTER];
                c.Radius = (double)properties[CIRCLE_RADIUS];
            }
        }
    }
 
    public static void SendMessageToCommandLine(string msg)
    {
        var ed = Autodesk.AutoCAD.ApplicationServices.Application.
            DocumentManager.MdiActiveDocument.Editor;
        ed.WriteMessage(msg);
    }
}

Following CommandClass put all together into work:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(FreezePropertyOverrule.MyCommands))]
 
namespace FreezePropertyOverrule
{
    public class MyCommands 
    {
        private static PropertyFreezeOverrule _freezeOverrule = null;
 
        [CommandMethod("StartFreeze")]
        public static void StartMyOverrule()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            if (_freezeOverrule==null)
            {
                _freezeOverrule = new PropertyFreezeOverrule();
            }
 
            var targets = new EntityTarget[]
            {
                new EntityTarget
                {
                    EntityType=typeof(Line), 
                    PropExtractFunc=PropertyExtractRestoreUtils.GetLineProperties, 
                    PropRestoreAction=PropertyExtractRestoreUtils.RestoreLineProperties 
                },
                new EntityTarget
                {
                    EntityType=typeof(Circle),
                    PropExtractFunc=PropertyExtractRestoreUtils.GetCircleProperties,
                    PropRestoreAction=PropertyExtractRestoreUtils.RestoreCircleProperties
                }
            };
 
            _freezeOverrule.Start(targets);
            ed.WriteMessage(
                "\nEntity freezing overrule started\n");
        }
 
        [CommandMethod("StopFreeze")]
        public static void StopMyOverrule()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
 
            var ed = dwg.Editor;
            if (_freezeOverrule!=null)
            {
                _freezeOverrule.Stop();
            }
 
            ed.WriteMessage(
                "\nEntity freezing overrule stopped.\n");
        }
    }
}

The video clip below shows how the code works: if the Overrule is enabled (started), Line entity cannot be extended, shortened, moved, rotated..., while Circle also cannot be enlarged, shrunk, or moved. However, their other non-geometric properties, such as Layer, Color..., can still be changed. Once the Overrule is disabled/stopped, Line and Circle can be fully modified.



Wednesday, March 18, 2020

Showing Helpful Information As Tool Tip During Jig Dragging - 2

In the first part of this topic, I have built a quite simple moving jig by handling Editor.PointMonitor, where Transient Graphics is used to show a ghost image as the dragging effect. Because of PointMonitorEventArgs, it is really easy to show custom tool tip to provide useful information that would help user to decide where/how to drag an entity.

At this point, I have a good working moving jig that could prompt some information during entity dragging. However a new issue comes: how to make the jig to show different information as tool tip, based on business workflow, without having to modify the jig's code? When mouse cursor is at an entity during dragging, we now can get the entity's ObjectId, then we could obtain different information according to business requirements and show the information as tool tip, if necessary. Obviously, we do not want to modify the code in the PointMonitor event handler whenever there is different business requirement.

The approach to solve this is to inject a predefined tool tip generating interface functions, and the interface functions are implemented/coded outside the jig class. Following is the updated jig class code (red lines are the changes).

using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
 
namespace JigWithTooltip
{
    public class TooltipMovingJig2
    {
        private Document _dwg = null;
        private Editor _ed = null;
 
        private Entity _ghost = null;
        private Entity _entity = null;
        private Point3d _basePoint = Point3d.Origin;
        private Point3d _mousePoint = Point3d.Origin;
 
        private TransientManager _tsManager = 
            TransientManager.CurrentTransientManager;
 
        private Func<ObjectId, Point3d, string> _tipExtractFunction = null;
        private Func<ObjectId, bool> _isTooltipTargetFunc = null;
 
        public TooltipMovingJig2(Document dwg)
        {
            _dwg = dwg;
            _ed = dwg.Editor;
        }
 
        public void MoveEntity(
            Func<ObjectId, Point3d, string> tipExtractFunc = null,
            Func<ObjectId, bool> isTooltipTargetFunc = null)
        {
            if (!SelectEntity(out ObjectId entIdout _basePoint)) return;
 
            _tipExtractFunction = tipExtractFunc;
            _isTooltipTargetFunc = isTooltipTargetFunc;
 
            _mousePoint = _basePoint;
 
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                _entity = (Entity)tran.GetObject(entIdOpenMode.ForRead);
                _entity.Highlight();
                try
                {
                    if (GetDestinationPoint(out Point3d destPoint))
                    {
                        var mt = Matrix3d.Displacement(
                            _basePoint.GetVectorTo(destPoint));
                        _entity.UpgradeOpen();
                        _entity.TransformBy(mt);
                    }
                }
                finally
                {
                    _entity.Unhighlight();
                }
 
                tran.Commit();
            }
        }
 
        #region private methods
 
        private bool SelectEntity(out ObjectId entIdout Point3d basePoint)
        {
            entId = ObjectId.Null;
            basePoint = Point3d.Origin;
 
            var res = _ed.GetEntity("\nSelect entity to move:");
            if (res.Status == PromptStatus.OK)
            {
                entId = res.ObjectId;
                basePoint = res.PickedPoint;
 
                var opt = new PromptPointOptions(
                    "\nSelect base point:");
 
                var pRes = _ed.GetPoint(opt);
                if (pRes.Status == PromptStatus.OK)
                {
                    basePoint = pRes.Value;
                }
 
                return true;
            }
            else
            {
                return false;
            }
        }
 
        private void CreateMovingGhost()
        {
            ClearMovingGhost();
 
            _ghost = _entity.Clone() as Entity;
            _ghost.ColorIndex = 2;
            var mt = Matrix3d.Displacement(_basePoint.GetVectorTo(_mousePoint));
            _ghost.TransformBy(mt);
 
            _tsManager.AddTransient(
                _ghost, 
                TransientDrawingMode.DirectTopmost, 
                128, 
                new IntegerCollection());
        }
 
        private void ClearMovingGhost()
        {
            if (_ghost != null)
            {
                _tsManager.EraseTransient(_ghost, new IntegerCollection());
                _ghost.Dispose();
                _ghost = null;
            }
        }
 
        private bool GetDestinationPoint(out Point3d destPoint)
        {
            destPoint = Point3d.Origin;
            var picked = false;
 
            var opt = new PromptPointOptions(
                "Move to:");
            opt.UseBasePoint = true;
            opt.BasePoint = _basePoint;
            opt.UseDashedLine = true;
 
            // Set system variable "PICKBOX" to at least 10 (range 0 to 20)
            // so that mouse cursor would pick up entities easily 
            // when moveving close
            var pickBox = Convert.ToInt32(
                Application.GetSystemVariable("PICKBOX"));
            bool pickBoxChanged = false;
            if (pickBox < 10)
            {
                Application.SetSystemVariable("PICKBOX", 10);
                pickBoxChanged = true;
            }
 
            var forcedCount = _ed.TurnForcedPickOn();
            _ed.PointMonitor += Editor_PointMonitor;
 
            try
            {
                var res = _ed.GetPoint(opt);
                if (res.Status == PromptStatus.OK)
                {
                    destPoint = res.Value;
                    picked = true;
                }
            }
            finally
            {
                ClearMovingGhost();
                _ed.PointMonitor -= Editor_PointMonitor;
 
                var count = _ed.TurnForcedPickOff();
                while (count > forcedCount - 1)
                {
                    count = _ed.TurnForcedPickOff();
                }
 
                // restore "PICKBOX" original value
                if (pickBoxChanged)
                    Application.SetSystemVariable("PICKBOX"pickBox);
            }
 
            return picked;
        }
 
        private void Editor_PointMonitor(object senderPointMonitorEventArgs e)
        {
            _mousePoint = e.Context.RawPoint;
            CreateMovingGhost();
 
            var paths = e.Context.GetPickedEntities();
            if (paths == null || paths.Length == 0) return;
            var ids = paths[0].GetObjectIds();
            if (ids == null || ids.Length == 0) return;
 
            var id = ids[0];
 
            if (_isTooltipTargetFunc != null)
            {
                if (!_isTooltipTargetFunc(id))
                {
                    id = ObjectId.Null;
                }
            }
            
            if (!id.IsNull)
            {
                var tip = GetDefaultTooltip(id);
                if (_tipExtractFunction != null)
                {
                    tip = _tipExtractFunction(id, _mousePoint);
                }
 
                e.AppendToolTipText(tip);
            }
        }
 
        private string GetDefaultTooltip(ObjectId entId)
        {
            return $"\nMove to/close to:{entId.ObjectClass.DxfName.ToUpper()}";
        }
 
        #endregion
    }
}

As the code shows, the jig class now has 2 Functions as its member, which can be injected from the jig's calling procedure. One function is to determine if an entity is the target entity that I want to show custom tool tip; the other is to generate actual tool tip content. Both function take ObjectId as input parameter; and the tool til generating function also takes a Point3d input as parameter, which is where the mouse cursor is and may be needed for specific tool tip content.

The jig class only defines the 2 function's signature (interface). The actual implementations of the 2 functions are done outside the jig class. They are injected into the jig class when the jig's public method MoveEntity() is called. Thus I am free to write different functions to determine whether an entity is the tool tip showing target and what tool tip content to be generated against the entity.

Following are 2 pairs of these functions: one pair is to test if the entity is closed polyline, if yes, get its area information as tool tip; the other pair - to test if the entity is curve, and show the distance of mouse cursor point to the curve's start point as tool tip information.

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
 
namespace JigWithTooltip
{
    public class EntityToolTipHelper
    {
        public static bool IsAreaToolTipTarget(ObjectId entId)
        {
            return entId.ObjectClass == RXClass.GetClass(typeof(Polyline));
        }
 
        public static string ExtractAreaToolTip(ObjectId entIdPoint3d mouseLocation)
        {
            if (entId.ObjectClass != RXClass.GetClass(typeof(Polyline))) return "";
 
            var tip = "";
 
            using (var tran = new OpenCloseTransaction())
            {
                var poly = (Polyline)tran.GetObject(entIdOpenMode.ForRead);
                if (poly.Closed)
                {
                    tip =$"AREA: {poly.Area.ToString("##########0.00")}";
                }
            }
 
            return tip;
        }
 
        public static bool IsDistanceToolTipTarget(ObjectId entId)
        {
            return entId.ObjectClass.IsDerivedFrom(RXClass.GetClass(typeof(Curve)));
        }
 
        public static string ExtractDistanceToolTip(ObjectId entIdPoint3d mousePoint)
        {
            if (!entId.ObjectClass.IsDerivedFrom(RXClass.GetClass(typeof(Curve)))) return "";
 
            var tip = "";
 
            using (var tran = new OpenCloseTransaction())
            {
                var curve = (Curve)tran.GetObject(entIdOpenMode.ForRead);
                var pt = curve.GetClosestPointTo(mousePointfalse);
                var dist = curve.GetDistAtPoint(pt);
 
                tip = $"DISTANCE FROM START POINT: {dist.ToString("########0.00")}";
            }
 
            return tip;
        }
    }
}

Now I can have a command to run the moving jig with area being prompted and another command to run the moving jig with distance being prompted:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(JigWithTooltip.MyCommands))]
 
namespace JigWithTooltip
{
    public class MyCommands
    {
        [CommandMethod("DoMove")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var mover = new TooltipMovingJig2(dwg);
                mover.MoveEntity();
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nInitializing error:\n{ex.Message}\n");
            }
        }
 
        [CommandMethod("MoveToArea")]
        public static void MoveWithAreaPrompt()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var mover = new TooltipMovingJig2(dwg);
                mover.MoveEntity(
                    EntityToolTipHelper.ExtractAreaToolTip,
                    EntityToolTipHelper.IsAreaToolTipTarget);
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nInitializing error:\n{ex.Message}\n");
            }
        }
 
        [CommandMethod("MoveToDistance")]
        public static void MoveWithDistancePrompt()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var mover = new TooltipMovingJig2(dwg);
                mover.MoveEntity(
                    EntityToolTipHelper.ExtractDistanceToolTip,
                    EntityToolTipHelper.IsDistanceToolTipTarget);
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nInitializing error:\n{ex.Message}\n");
            }
        }
    }
}

As the code shows, it is very easy to implement a pair of functions outside the jig class code to make the jig smart enough to decide whether custom tool tip is wanted, and what tool tip content is to appear, Here the a video clip showing the visual effect of running the commands:


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.