I wasn’t expecting to write a third part in this series of posts, but then Samir Bittar went and asked a follow-up question that I felt obliged to look into. Thanks for the suggestion, Samir! :-)
Samir basically wanted to provide the user with more feedback as they’re selecting the nested entity – so that the sub-entity gets highlighted, rather than the full block reference.
This turned out to be quite a tricky scenario to address. The overall approach I used was to use a PointMonitor to perform a non-interactive, nested selection of the geometry beneath the cursor and then highlight the results. The trick was how best to actually perform the selection itself. The options – with their pros and cons – came down to this:
- Use Editor.GetNestedEntity() to perform the selection.
- Nice because you get the square selection cursor displayed.
- Re-entering the function non-interactively from a PointMonitor causes strange behaviour with the dynamic input tooltip (possibly just one symptom of something that’s fundamentally unstable).
- Use Editor.GetEntity() to perform the selection.
- Similar behaviour to 1, without the benefits of selecting the sub-entity.
- Use a basic jig and Editor.Drag() to acquire a point.
- Also allows you to select the square selection cursor.
- Stops the sub-entity highlighting from working at all.
- Use Editor.GetPoint() to get a raw point.
- Works fine with non-interactive, nested entity selection.
- You don’t get the nice, square selection cursor.
- You need to disable object snap, to avoid further confusion.
This list may well be missing some options – please do post a comment, if so.
After working through 1-3, I settled on 4. I didn’t like the fact you don’t get the selection cursor, but it seemed the lesser of the various evils. (I did actually try to replace the system cursor with my own inside the PointMonitor, but this just ended up adding an additional cursor and causing some strange lag effects between it and the standard AutoCAD cross-hairs.)
Here’s the C# code that implements this in the MTIBJIGHL command (short for MoveTextInBlockJigHighLight ;-):
using System;
using System.Runtime.InteropServices;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
namespace Textformations
{
public class DisplacementJig2 : EntityJig
{
private Point3d _pos;
private Point3d _loc;
public DisplacementJig2(Entity ent, Point3d basePt)
: base(ent)
{
_loc = basePt;
}
protected override bool Update()
{
var disp = _pos - _loc;
_loc = _pos;
var mat = Matrix3d.Displacement(disp);
Entity.TransformBy(mat);
return true;
}
protected override SamplerStatus Sampler(JigPrompts prompts)
{
var opts =
new JigPromptPointOptions("\nSelect displacement");
opts.BasePoint = _pos;
opts.UserInputControls =
UserInputControls.NoZeroResponseAccepted;
var ppr = prompts.AcquirePoint(opts);
if (_pos == ppr.Value)
return SamplerStatus.NoChange;
_pos = ppr.Value;
return SamplerStatus.OK;
}
}
public class SubEntityHighlighter
{
[DllImport(
"accore.dll",
CallingConvention = CallingConvention.Cdecl
)]
private static extern int acedRedraw(
ref ads_name name, int mode
);
[DllImport(
"acdb19.dll",
CallingConvention=CallingConvention.Cdecl,
EntryPoint=
"?acdbGetAdsName@@YA?AW4ErrorStatus@Acad@@AEAY01_JVAcDbObjectId@@@Z"
)]
private static extern int acdbGetAdsName(
ref ads_name name, ObjectId objId
);
public struct ads_name
{
IntPtr a;
IntPtr b;
};
private Document _doc;
private FullSubentityPath _path;
private ObjectId _entId;
private ObjectId _selId;
private ObjectId[] _objIds = null;
public ObjectId LastSelected
{
get { return _selId; }
}
public SubEntityHighlighter(Document doc)
{
_doc = doc;
_entId = ObjectId.Null;
_path = FullSubentityPath.Null;
}
internal void Unhighlight()
{
if (
_selId.ObjectClass.IsDerivedFrom(
RXClass.GetClass(typeof(AttributeReference))
)
)
{
// For attributes we need to use acedRedraw to unhighlight
var ename = new ads_name();
acdbGetAdsName(ref ename, _selId);
acedRedraw(ref ename, 4);
return;
}
if (!_path.IsNull)
{
var tr =
_doc.TransactionManager.StartOpenCloseTransaction();
using (tr)
{
var ent = (Entity)tr.GetObject(_entId, OpenMode.ForRead);
// ... and highlight the nested entity
ent.Unhighlight(_path, false);
tr.Commit();
_path = FullSubentityPath.Null;
_entId = ObjectId.Null;
_selId = ObjectId.Null;
}
}
}
internal void Highlight(
FullSubentityPath path, ObjectId entId, ObjectId selId
)
{
Unhighlight();
if (
selId.ObjectClass.IsDerivedFrom(
RXClass.GetClass(typeof(AttributeReference))
)
)
{
// For attributes we need to use acedRedraw to highlight
var ename = new ads_name();
acdbGetAdsName(ref ename, selId);
acedRedraw(ref ename, 3);
}
else
{
var tr =
_doc.TransactionManager.StartOpenCloseTransaction();
using (tr)
{
var ent = (Entity)tr.GetObject(entId, OpenMode.ForRead);
// Highlight the nested entity
ent.Highlight(path, false);
tr.Commit();
}
}
_path = path;
_entId = entId;
_selId = selId;
}
internal FullSubentityPath BuildSubEntityPath(
PromptNestedEntityResult rs,
out ObjectId outer
)
{
// Get the "containers" list. We store this as member data
// to save getting it twice. This is the only reason this
// method is not static...
_objIds = rs.GetContainers();
var ensel = rs.ObjectId;
int len = _objIds.Length;
ObjectId[] revIds;
// Reverse the "containers" list
revIds = new ObjectId[len + 1];
for (int i = 0; i < len; i++)
{
var id = (ObjectId)_objIds.GetValue(len - i - 1);
revIds.SetValue(id, i);
}
// Now add the selected entity to the end
revIds.SetValue(ensel, len);
outer = revIds[0];
// Retrieve the sub-entity path for this entity
var subEnt =
new SubentityId(SubentityType.Null, System.IntPtr.Zero);
return new FullSubentityPath(revIds, subEnt);
}
internal ObjectId[] GetContainers()
{
return _objIds;
}
}
public class Commands3
{
[CommandMethod("MTIBJIGHL")]
public 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 sehl = new SubEntityHighlighter(doc);
PointMonitorEventHandler handler =
delegate(object s, PointMonitorEventArgs e)
{
// Programmatically select the nested entity under
// the cursor
var pneo = new PromptNestedEntityOptions("");
pneo.NonInteractivePickPoint = e.Context.ComputedPoint;
pneo.UseNonInteractivePickPoint = true;
var rs = ed.GetNestedEntity(pneo);
if (rs.Status != PromptStatus.OK)
{
// If there wasn't anything there (very common)
// we'll unhighlight anything highlighted
sehl.Unhighlight();
return;
}
// So we have something under the cursor: get the path
// to it as well as the outermost entity ID
ObjectId outer;
var path = sehl.BuildSubEntityPath(rs, out outer);
// Highlight the sub-entity
sehl.Highlight(path, outer, rs.ObjectId);
};
// Set the object snap mode to 0, to be reset afterwards
var os = (short)Application.GetSystemVariable("OSMODE");
Application.SetSystemVariable("OSMODE", 0);
// Ask for a point to be selected: we'll manually worry about
// the pseudo-selection process
var ppo = new PromptPointOptions("\nSelect text inside block");
// Add our PointMonitor around the point selection call
ed.PointMonitor += handler;
var ppr = ed.GetPoint(ppo);
ed.PointMonitor -= handler;
// Reset the object snap mode right away
Application.SetSystemVariable("OSMODE", os);
// Get the last entity selected (Null if nothing)
var selId = sehl.LastSelected;
// If we don't have a valid ObjectId, don't continue
if (ppr.Status != PromptStatus.OK || selId == ObjectId.Null)
{
sehl.Unhighlight();
return;
}
// Check the type of object we're dealing with
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 pkr = ed.GetKeywords(pko);
if (
pkr.Status != PromptStatus.OK || pkr.StringResult == "No"
)
{
sehl.Unhighlight();
return;
}
}
// Start a transaction to modify the object
using (var tr = doc.TransactionManager.StartTransaction())
{
// 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 = sehl.GetContainers();
foreach (var brId in conts)
{
var br =
tr.GetObject(brId, OpenMode.ForRead) as BlockReference;
if (br != null)
{
brMat = brMat.PreMultiplyBy(br.BlockTransform);
}
}
// Transform the entity
var ent = (Entity)tr.GetObject(selId, OpenMode.ForWrite);
// Before we run the jig, transform the object by the
// aggregate transform of the containers
ent.TransformBy(brMat);
// Run the jig to displace the object
var dj = new DisplacementJig2(ent, ppr.Value);
var pr2 = ed.Drag(dj);
sehl.Unhighlight();
if (pr2.Status == PromptStatus.OK)
{
// We can transform the entity back, now (at least in
// terms of the containers: the displacement remains)
ent.TransformBy(brMat.Inverse());
// 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 the command in action, to give you a sense of the results:
In order to deal with a problem with highlighting attributes, I did end up having to P/Invoke acdbGetAdsName() and acedRedraw(), which I’m not exactly thrilled about. If you don’t care about attributes, I strongly suggest ripping that code out. And it only actually works for top-level attributes, anyway: attributes in nested blocks won’t get highlighted with it, unfortunately.
If you end up looking into this and finding a nicer way to achieve Samir’s request, please do let me know. I started out implementing something using a fairly standard technique – point selection followed by non-interactive nested entity selection – but admittedly the final solution has become messier than I’d have liked. It’s very possible someone out there has managed to find a way to get this working perfectly (or better, anyway), in which case I’d very much like to hear from you.