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
{
public class Commands
{
[CommandMethod("MTIB")]
public static void 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 =
new PromptNestedEntityOptions("\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 =
new PromptKeywordOptions(
"\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 = new PromptPointOptions("\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) as BlockReference;
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) as Entity;
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.