December 02, 2013

Moving text in an AutoCAD block using .NET – Part 1

I had this question come in from Bruce Gordon by email a couple of weeks ago, and it seemed like a fun one to look at:

Is it possible to write a utility to pick a text entity in a block reference and move it to a new location?

As a first step, I put together some code that launches a nested entity selection and then performs a translation of the selected entity by a specified displacement from the picked point.

Given the requirement in the original request, the code works “first time” for text entities (DBText and MText), but asks the user for confirmation if a non-text entity is selected. As the displacement is performed via a standard matrix transformation – and every Entity has its TransformBy() method – this is a somewhat artificial distinction and can be removed very easily: it works very well with non-text too, of course. The transform needs a little extra work to work with nested (and rotated) blocks, in particular: we need to aggregate the block transforms of the containing block references and use those to get transformation into the equivalent of world coordinates. We can do this by pre- and post-multiplying the actual displacement matrix with the appropriate transforms. Comments in the code should help explain the specifics.

This type of command – one that modifies a shared definition that might be instanced multiple times (which is certainly the case with block definitions and their references) – always has the risk of causing user confusion. If you’re going to implement this in the wild then I suggest doing such things as checking the number of references to the block definitions you’re modifying so that the user can be informed of changes that might otherwise lead to head-scratching.

This version of the code doesn’t display the entity being dragged across – we do a standard GetPoint() having set the base point to get rubber-banding of the point selection, only – but we’ll add a jig in Part 2 that shows the entity being moved.

Here’s the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

namespace Textformations

{

publicclassCommands

{

[CommandMethod("MTIB")]

publicstaticvoid MoveTextInBlock()

{

var doc = Application.DocumentManager.MdiActiveDocument;

var db = doc.Database;

var ed = doc.Editor;

// Start by getting the text (or other) object in the block

var pneo =

newPromptNestedEntityOptions("\nSelect text inside block");

var pner = ed.GetNestedEntity(pneo);

if (pner.Status != PromptStatus.OK)

return;

var selId = pner.ObjectId;

// Start a transaction to access the object

var oc = selId.ObjectClass;

if (

!oc.IsDerivedFrom(

RXClass.GetClass(typeof(DBText))

) &&

!oc.IsDerivedFrom(

RXClass.GetClass(typeof(MText))

)

)

{

// Isn't a text object - ask whether we continue

ed.WriteMessage(

"\nObject is not text, it is a {0}.", oc.Name

);

var pko =

newPromptKeywordOptions(

"\nDo you want to continue? [Yes/No]", "Yes No"

);

pko.AppendKeywordsToMessage = true;

pko.AllowNone = true;

pko.Keywords.Default = "No";

var pr = ed.GetKeywords(pko);

if (

pr.Status != PromptStatus.OK || pr.StringResult == "No"

)

return;

}

// Now get the displacement from the picked point

var ppo = newPromptPointOptions("\nEnter displacement");

ppo.BasePoint = pner.PickedPoint;

ppo.UseBasePoint = true;

var ppr = ed.GetPoint(ppo);

if (ppr.Status != PromptStatus.OK)

return;

// Start a transaction to modify the object

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

{

// Get the displacement from the picked point to the newly

// selected one

var disp = ppr.Value - pner.PickedPoint;

var mat = Matrix3d.Displacement(disp);

// Unless we get a block reference container, use the

// identity matrix as the block transform

var brMat = Matrix3d.Identity;

// Get the containers around the nested entity

var conts = pner.GetContainers();

foreach (var id in conts)

{

var br =

tr.GetObject(id, OpenMode.ForRead) asBlockReference;

if (br != null)

{

brMat = brMat.PreMultiplyBy(br.BlockTransform);

}

}

// To get the transformation in world coordinates rather

// than block coordinates we post-multiply by block

// transform and pre-multiply by its inverse

mat = mat.PreMultiplyBy(brMat.Inverse());

mat = mat.PostMultiplyBy(brMat);

// Transform the entity

var ent = (Entity)tr.GetObject(selId, OpenMode.ForWrite);

ent.TransformBy(mat);

// Open each of the containers and set a property so that

// they each get regenerated

foreach (var id in conts)

{

var ent2 = tr.GetObject(id, OpenMode.ForWrite) asEntity;

if (ent2 != null)

{

// We might also have called this method:

// ent2.RecordGraphicsModified(true);

// but setting a property works better with undo

ent2.Visible = ent2.Visible;

}

}

tr.Commit();

}

}

}

}

Here’s a quick video of the command in action:

Thanks to Bruce Gordon for asking an interesting question and then helping test this implementation. It’s certainly an interesting technique that can be used in lots of different scenarios.

I’m now at AU2013 and will probably post about the conference over the coming days, so we’ll see when I get to post the follow-up post that adds preview graphics via a jig.