Monday, June 25, 2018

Change Width of DBText/AttributeReference by Its WidthFactor - Part Two: Drag-Fitting

In previous post, I showed the code to automatically adjust DBText's width to fit into a given space by reducing DBText's WidthFactor. Then I thought why not give user another handy way to adjust DBText's width: simply drag the DBText entity to desired width, much similar to AutoCAD's built-in dragging to scale an entity (but in this case, only width of DBText entity is "scaled").

Now that I am talking about dragging entities, I could derive my process from Jig class, or just build my own "Jig" style process with Transient graphics. I decided to go with the latter.

Here is the code:

using System;
 
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.GraphicsInterface;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace FitAttributeWidth
{
    public class TextWidthDragger
    {
        private Document _dwg;
        private Editor _ed;
        private ObjectId _txtId = ObjectId.Null;
        private double _factor = 1.0;
        private double _width = 0.0;
        private DBText _textDrawable = null;
        private Point3d _basePoint;
 
        private TransientManager _tsManager = TransientManager.CurrentTransientManager;
 
 
        public TextWidthDragger()
        {
            
        }
 
        public void DragTextEntity()
        {
            _dwg = CadApp.DocumentManager.MdiActiveDocument;
            _ed = _dwg.Editor;
 
            var entId = SelectDragTarget();
            if (entId.IsNull)
            {
                _ed.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            DragDBText(_dwg, entId);
        }
 
        public void DragDBText(Document dwg, ObjectId txtId)
        {
            _dwg = dwg;
            _ed = dwg.Editor;
            _txtId = txtId;
 
            DoTextDrag();
        }
 
        #region private methods: select DBText or AttributeReference
 
        private ObjectId SelectDragTarget()
        {
            var entId = ObjectId.Null;
 
            var opt = new PromptKeywordOptions(
                "\nSelect a Text or an Attribute in block:");
            opt.AppendKeywordsToMessage = true;
            opt.Keywords.Add("Text");
            opt.Keywords.Add("Attribute");
            opt.Keywords.Default = "Text";
 
            var res = _ed.GetKeywords(opt);
            if (res.Status== PromptStatus.OK)
            {
                if (res.StringResult == "Text")
                    entId = SelectDBText();
                else
                    entId = SelectAttribute();
            }
 
            return entId;
        }
 
        private ObjectId SelectDBText()
        {
            var txtId = ObjectId.Null;
 
            var opt = new PromptEntityOptions(
                "\nSelect a Text entity:");
            opt.SetRejectMessage("\nInvalid selection: not a text entity!");
            opt.AddAllowedClass(typeof(DBText), true);
 
            var res = _ed.GetEntity(opt);
            if (res.Status== PromptStatus.OK)
            {
                txtId = res.ObjectId;
            }
 
            return txtId;
        }
 
        private ObjectId SelectAttribute()
        {
            var attId = ObjectId.Null;
 
            while (true)
            {
                var opt = new PromptNestedEntityOptions(
                    "\nSelect an Attribute in block:");
                opt.AllowNone = false;
 
                var res = _ed.GetNestedEntity(opt);
                if (res.Status == PromptStatus.OK)
                {
                    if (res.ObjectId.ObjectClass.DxfName.ToUpper() == "ATTRIB")
                    {
                        attId = res.ObjectId;
                    }
                    else
                    {
                        _ed.WriteMessage(
                            "\nInvalid selection: not an attribue in block!");
                    }
                }
                else
                {
                    break;
                }
 
                if (!attId.IsNull) break;
            }
 
            return attId;
        }
 
        #endregion
 
        #region private methods
 
        private void DoTextDrag()
        {
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var target = (DBText)tran.GetObject(_txtId, OpenMode.ForRead);
                _factor = target.WidthFactor;
                _width = GetTextWidth(target) / _factor;
                _basePoint = target.Position;
 
                bool dragOk = false;
                try
                {
                    _textDrawable = (DBText)target.Clone();
                    _textDrawable.ColorIndex = 2;
 
                    _ed.PointMonitor += Editor_PointMonitor;
 
                    var dist = GetDistance();
                    if (dist!=double.MinValue)
                    {
                        _factor = dist / _width;
                        target.UpgradeOpen();
                        target.WidthFactor = _factor;
                    }
                }
                finally
                {
                    ClearTransients();
                    _textDrawable.Dispose();
                    _ed.PointMonitor -= Editor_PointMonitor;
                }
 
                if (dragOk && _factor!=target.WidthFactor)
                {
                    target.UpgradeOpen();
                    target.WidthFactor = _factor;
                }
 
                tran.Commit();
            }
        }
 
        private double GetTextWidth(DBText txt)
        {
            var ext = txt.GeometricExtents;
            return Math.Abs(ext.MaxPoint.X - ext.MinPoint.X);
        }
 
        private void Editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            ClearTransients();
 
            var dist = e.Context.RawPoint.DistanceTo(_basePoint);
            _factor = dist / _width;
 
            e.AppendToolTipText($"Width scale factor: {_factor}");
 
            _textDrawable.WidthFactor = _factor;
 
            AddTransients();
        }
 
        private void AddTransients()
        {
            _tsManager.AddTransient(
                _textDrawable, 
                TransientDrawingMode.Highlight, 
                128, 
                new IntegerCollection());
        }
 
        private void ClearTransients()
        {
            _tsManager.EraseTransient(_textDrawable, new IntegerCollection());
        }
 
        private double GetDistance()
        {
            var opt = new PromptDistanceOptions(
                "\nSet text entity width scale factor:");
            opt.AllowZero = false;
            opt.AllowNegative = false;
            opt.AllowArbitraryInput = false;
            opt.AllowNone = false;
            opt.UseBasePoint = true;
            opt.BasePoint = _basePoint;
            opt.UseDashedLine = true;
 
            var res = _ed.GetDistance(opt);
            if (res.Status == PromptStatus.OK)
                return res.Value;
            else
                return double.MinValue;
        }
 
        #endregion
    }
}

Here is the command to run it:

[CommandMethod("DragTextWidth")]
public static void DragTextWidth()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    try
    {
        var dragger = new TextWidthDragger();
        dragger.DragTextEntity();
    }
    catch (System.Exception ex)
    {
        ed.WriteMessage("\nError:\n{0}.", ex.Message);
    }
    finally
    {
        Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
    }
}

Again, as mentioned in previous post, for simplicity, when calculate entity's width I assume the DBText/AttributeReference entity is placed horizontally. The code also tries to show the DBText's WidthFactor dynamically as tooltip when the DBText entity is dragged. However, since the tooltip thing is handled in PointMonitor event in the simplest way, the tooltip only shows when mouse cursor stays not moving. It might be possible to use the same or similar approach showed in this post to make a good tooltip to show changing WidthFactor while dragging. Again, for the simplicity of this post, I decide not spend time on this for now.

See this video clip showing how the code works.


Change Width of DBText/AttributeReference by Its WidthFactor - Part One: Auto-Fitting

When a text entity (DBText) is added int drawing, very often there is only limited space available to place the text entity (mainly the space would limit the width of the text entity). In the case when text entity is created by AutoCAD user, if the text entity's width exceeds the allowed space, user can either change the text entity's text string by using different words/characters without losing the text string's meaning, or change the text entity's WidthFactor (assuming the text entity's style and height remain) so that the characters of the text string are rendered narrower than normal.

Changing text entity's overall width with its WidthFactor from Properties window is an easy and straightforward "try-and-see" process. However, in the case of attribute in a block (AttributeReference, which is derived from DBText, hence its overall widht can also be changed with its WidthFactor), it would be not that easy/straightforward to set its WidthFactor via Properties window.

Furthermore, as AutoCAD programmers, we often programmatically set text string of DBText or AttributeReference entities. In this case, we usually do not have the freedom to change the words/characters of the text string to reduce the overall width of the entities. Therefore, it would be natural to use code to automatically set WidthFactor to a proper value so that that the width of an DBText entity would fit a given space. For example, many drawing title blocks/borders have fields or tables to be filled with either DBTexts, or AttributeReferences programmatically. It would be good the application is able to adjust the width of DBText/AttributeReference to fit the text string in given space (width of the field or table column).

Assume I am in this situation: I need to use code to fill out a table with data from project database as text, or block's attribute. The text values could be too long to fit into the table's column width from time to time. So, as long as I know the width of the columns/fields, I want to shrink the text/attribute entity's width by change its WidthFactor automatically, so that user does not have to examine the table being updated by my application visually for the unfit texts.

The code to do the "auto-fit" work is really simple, shown below, as an extension method to ObjectId:

public static class TextWidthExtension
{
    public static bool FitTextWidth(
        this ObjectId entId, double width, double minWidthFactor=0.5)
    {
        var dfxName = entId.ObjectClass.DxfName.ToUpper();
        if (dfxName!="ATTRIB" && dfxName!="TEXT")
        {
            throw new ArgumentException(
                "Entity must be either DBText, or AttributeReference!");
        }
 
        bool fit = false;
 
        using (var tran = entId.Database.TransactionManager.StartTransaction())
        {
            var txtEnt = (DBText)tran.GetObject(entId, OpenMode.ForWrite);
            fit = FitToWidth(txtEnt, width, minWidthFactor);
            if (fit)
                tran.Commit();
            else
                tran.Abort();
        }
 
        return fit;
    }
 
    private static bool FitToWidth(DBText txt, double maxWidth, double minWidFactor)
    {
        var txtW = GetTextWidth(txt);
        bool fit = true;
        double factor = txt.WidthFactor;
 
        while (txtW > maxWidth && factor >= minWidFactor)
        {
            fit = false;
            factor -= 0.05;
            txt.WidthFactor = factor;
            txtW = GetTextWidth(txt);
        }
 
        fit = txtW < maxWidth;
 
        return fit;
    }
 
    private static double GetTextWidth(DBText txt)
    {
        var ext = txt.GeometricExtents;
        return Math.Abs(ext.MaxPoint.X - ext.MinPoint.X);
    }
}

This method takes an optional input to indicate the minimum value of WidthFactor of an DBText. In reality, it would make little sense to set WidthFactor value too small. Also, in the shown code, for simplicity, I calculate the width of a DBText entity with its GeometricExtents, assuming the DBText entity is horizontal (its Rotation=0.0). I could have also made the decrement of the WdithFactor in the "while{...}" loop a input parameter of the method instead of hard-coded as "0.05".

Here is the command to run the code:

using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(FitAttributeWidth.MyCommands))]
 
namespace FitAttributeWidth
{
    public class MyCommands
    {
        [CommandMethod("FitAtts")]
        public static void FitAttributeToSpace()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var attIds = GetBlockAttributeIds(ed);
                if (attIds.Count()>0)
                {
                    foreach (var id in attIds)
                    {
                        id.FitTextWidth(60.0, 0.7);
                    }
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError:\n{0}.", ex.Message);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        [CommandMethod("FitText")]
        public static void FitTextToSpace()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                while(true)
                {
                    var txtId = SelectText(ed);
                    if (!txtId.IsNull)
                    {
                        txtId.FitTextWidth(60.0, 0.7);
                    }
                    else
                    {
                        break;
                    }
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError:\n{0}.", ex.Message);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        #region private methods
 
        private static IEnumerable<ObjectId> GetBlockAttributeIds(Editor ed)
        {
            var lst = new List<ObjectId>();
 
            var opt = new PromptEntityOptions(
                "\nSelect the block for fitting its attributes:");
            opt.SetRejectMessage("\nInvalid selection: not a block.");
            opt.AddAllowedClass(typeof(BlockReference), true);
            var res = ed.GetEntity(opt);
            if (res.Status==PromptStatus.OK)
            {
                using (var tran = res.ObjectId.Database.TransactionManager.StartTransaction())
                {
                    var bref = (BlockReference)tran.GetObject(res.ObjectId, OpenMode.ForRead);
                    if (bref.AttributeCollection.Count>0)
                    {
                        lst.AddRange(bref.AttributeCollection.Cast<ObjectId>());
                    }
                    tran.Commit();
                }
            }
 
            return lst;
        }
 
        private static ObjectId SelectText(Editor ed)
        {
            ObjectId id = ObjectId.Null;
 
            var opt = new PromptEntityOptions(
                "\nSelect a Text entity in the table:");
            opt.SetRejectMessage("\nInvalid: not a DBText!");
            opt.AddAllowedClass(typeof(DBText), true);
 
            var res = ed.GetEntity(opt);
            if (res.Status== PromptStatus.OK)
            {
                id = res.ObjectId;
            }
 
            return id;
        }
 
        #endregion
    }
}

This video clip shows the result of code execution.