Thanks, once again, to Philippe Leefsma, a DevTech engineer based in Prague, for contributing the code for this post. While researching an issue he was working on Philippe stumbled across a comment on this previous post where I more-or-less said jigging attributes wasn’t possible. Ahem. Anyway, Philippe decided to – quite rightly – prove me wrong, and the result is today’s post. :-)
It turns out that the trick to jigging a block with attributes is to add the block reference to the database prior to running the jig. I’d been coming at this from another direction – working out how to call through to the right version of the ObjectARX function, the one that allows the block reference to be in-memory rather than db-resident – but Philippe’s approach means that’s no longer needed. I see this technique as potentially being useful when jigging other entities that benefit from being database resident (Solid3d objects spring to mind), so I really appreciate Philippe’s hard work on this.
Here’s the C# code which I’ve edited for posting:
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using System.Collections.Generic;
namespace BlockJig
{
class CBlockJig : EntityJig
{
private Point3d _pos;
private Dictionary<string, Point3d> _attPos;
private Transaction _tr;
public CBlockJig(Transaction tr, BlockReference br)
: base(br)
{
_pos = br.Position;
// Initialize our dictionary with the tag /
// AttributeDefinition position
_attPos = new Dictionary<string, Point3d>();
_tr = tr;
BlockTableRecord btr =
(BlockTableRecord)_tr.GetObject(
br.BlockTableRecord,
OpenMode.ForRead
);
if (btr.HasAttributeDefinitions)
{
foreach (ObjectId id in btr)
{
DBObject obj =
tr.GetObject(id, OpenMode.ForRead);
AttributeDefinition ad =
obj as AttributeDefinition;
if (ad != null)
{
_attPos.Add(ad.Tag, ad.Position);
}
}
}
}
protected override bool Update()
{
BlockReference br = Entity as BlockReference;
br.Position = _pos;
if (br.AttributeCollection.Count != 0)
{
foreach (ObjectId id in br.AttributeCollection)
{
DBObject obj =
_tr.GetObject(id, OpenMode.ForRead);
AttributeReference ar =
obj as AttributeReference;
// Apply block transform to att def position
if (ar != null)
{
ar.UpgradeOpen();
ar.Position =
_attPos[ar.Tag].TransformBy(br.BlockTransform);
}
}
}
return true;
}
protected override SamplerStatus Sampler(JigPrompts prompts)
{
JigPromptPointOptions opts =
new JigPromptPointOptions("\nSelect insertion point:");
opts.BasePoint = new Point3d(0, 0, 0);
opts.UserInputControls =
UserInputControls.NoZeroResponseAccepted;
PromptPointResult ppr = prompts.AcquirePoint(opts);
if (_pos == ppr.Value)
{
return SamplerStatus.NoChange;
}
_pos = ppr.Value;
return SamplerStatus.OK;
}
public PromptStatus Run()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
PromptResult promptResult = ed.Drag(this);
return promptResult.Status;
}
}
public class Commands
{
[CommandMethod("BJ")]
static public void BlockJig()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
PromptStringOptions pso =
new PromptStringOptions("\nEnter block name: ");
PromptResult pr = ed.GetString(pso);
if (pr.Status != PromptStatus.OK)
return;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord space =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId,
OpenMode.ForRead
);
if (!bt.Has(pr.StringResult))
{
ed.WriteMessage(
"\nBlock \"" + pr.StringResult + "\" not found.");
return;
}
space.UpgradeOpen();
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
bt[pr.StringResult],
OpenMode.ForRead);
// Block needs to be inserted to current space before
// being able to append attribute to it
BlockReference br =
new BlockReference(new Point3d(), btr.ObjectId);
space.AppendEntity(br);
tr.AddNewlyCreatedDBObject(br, true);
if (btr.HasAttributeDefinitions)
{
foreach (ObjectId id in btr)
{
DBObject obj =
tr.GetObject(id, OpenMode.ForRead);
AttributeDefinition ad =
obj as AttributeDefinition;
if (ad != null && !ad.Constant)
{
AttributeReference ar =
new AttributeReference();
ar.SetAttributeFromBlock(ad, br.BlockTransform);
ar.Position =
ad.Position.TransformBy(br.BlockTransform);
ar.TextString = ad.TextString;
br.AttributeCollection.AppendAttribute(ar);
tr.AddNewlyCreatedDBObject(ar, true);
}
}
}
// Run the jig
CBlockJig myJig = new CBlockJig(tr, br);
if (myJig.Run() != PromptStatus.OK)
return;
// Commit changes if user accepted, otherwise discard
tr.Commit();
}
}
}
}
When you run the BJ command (short for BlockJig) and specify the name of a block in the current drawing which contains attributes, you’ll now see the attributes with their default values shown as part of the block being jigged. Implementing the code to allow editing of those attributes after insertion is left as an exercise for the reader.
Update:
This code didn't work for a few situations, such as when using justification (attributes would end up at the origin after being dragged) or with MText attributes (which would start at the origin until the mouse was moved).
A big thanks to Roland Feletic from PAUSER ZT-GMBH for helping identify and diagnose the various cases.
Here's the updated C# code:
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using System.Collections.Generic;
namespace BlockJigApplication
{
class AttInfo
{
private Point3d _pos;
private Point3d _aln;
private bool _aligned;
public AttInfo(Point3d pos, Point3d aln, bool aligned)
{
_pos = pos;
_aln = aln;
_aligned = aligned;
}
public Point3d Position
{
set { _pos = value; }
get { return _pos; }
}
public Point3d Alignment
{
set { _aln = value; }
get { return _aln; }
}
public bool IsAligned
{
set { _aligned = value; }
get { return _aligned; }
}
}
class BlockJig : EntityJig
{
private Point3d _pos;
private Dictionary<ObjectId, AttInfo> _attInfo;
private Transaction _tr;
public BlockJig(
Transaction tr,
BlockReference br,
Dictionary<ObjectId, AttInfo> attInfo
) : base(br)
{
_pos = br.Position;
_attInfo = attInfo;
_tr = tr;
}
protected override bool Update()
{
BlockReference br = Entity as BlockReference;
br.Position = _pos;
if (br.AttributeCollection.Count != 0)
{
foreach (ObjectId id in br.AttributeCollection)
{
DBObject obj =
_tr.GetObject(id, OpenMode.ForRead);
AttributeReference ar =
obj as AttributeReference;
// Apply block transform to att def position
if (ar != null)
{
ar.UpgradeOpen();
AttInfo ai = _attInfo[ar.ObjectId];
ar.Position =
ai.Position.TransformBy(br.BlockTransform);
if (ai.IsAligned)
{
ar.AlignmentPoint =
ai.Alignment.TransformBy(br.BlockTransform);
}
if (ar.IsMTextAttribute)
{
ar.UpdateMTextAttribute();
}
}
}
}
return true;
}
protected override SamplerStatus Sampler(JigPrompts prompts)
{
JigPromptPointOptions opts =
new JigPromptPointOptions("\nSelect insertion point:");
opts.BasePoint = new Point3d(0, 0, 0);
opts.UserInputControls =
UserInputControls.NoZeroResponseAccepted;
PromptPointResult ppr = prompts.AcquirePoint(opts);
if (_pos == ppr.Value)
{
return SamplerStatus.NoChange;
}
_pos = ppr.Value;
return SamplerStatus.OK;
}
public PromptStatus Run()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
PromptResult promptResult = ed.Drag(this);
return promptResult.Status;
}
}
public class Commands
{
[CommandMethod("BJ")]
static public void BlockJigCmd()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
PromptStringOptions pso =
new PromptStringOptions("\nEnter block name: ");
PromptResult pr = ed.GetString(pso);
if (pr.Status != PromptStatus.OK)
return;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
if (!bt.Has(pr.StringResult))
{
ed.WriteMessage(
"\nBlock \"" + pr.StringResult + "\" not found.");
return;
}
BlockTableRecord space =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId,
OpenMode.ForWrite
);
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
bt[pr.StringResult],
OpenMode.ForRead);
// Block needs to be inserted to current space before
// being able to append attribute to it
BlockReference br =
new BlockReference(new Point3d(), btr.ObjectId);
space.AppendEntity(br);
tr.AddNewlyCreatedDBObject(br, true);
Dictionary<ObjectId, AttInfo> attInfo =
new Dictionary<ObjectId,AttInfo>();
if (btr.HasAttributeDefinitions)
{
foreach (ObjectId id in btr)
{
DBObject obj =
tr.GetObject(id, OpenMode.ForRead);
AttributeDefinition ad =
obj as AttributeDefinition;
if (ad != null && !ad.Constant)
{
AttributeReference ar =
new AttributeReference();
ar.SetAttributeFromBlock(ad, br.BlockTransform);
ar.Position =
ad.Position.TransformBy(br.BlockTransform);
if (ad.Justify != AttachmentPoint.BaseLeft)
{
ar.AlignmentPoint =
ad.AlignmentPoint.TransformBy(br.BlockTransform);
}
if (ar.IsMTextAttribute)
{
ar.UpdateMTextAttribute();
}
ar.TextString = ad.TextString;
ObjectId arId =
br.AttributeCollection.AppendAttribute(ar);
tr.AddNewlyCreatedDBObject(ar, true);
// Initialize our dictionary with the ObjectId of
// the attribute reference + attribute definition info
attInfo.Add(
arId,
new AttInfo(
ad.Position,
ad.AlignmentPoint,
ad.Justify != AttachmentPoint.BaseLeft
)
);
}
}
}
// Run the jig
BlockJig myJig = new BlockJig(tr, br, attInfo);
if (myJig.Run() != PromptStatus.OK)
return;
// Commit changes if user accepted, otherwise discard
tr.Commit();
}
}
}
}
A few comments on this code:
- It's been refactored to make a single pass through the block definition to both create the block reference and collect the attribute information to store in our dictionary.
- The attribute information is now held in a class, which allows us to store more than just the position in our dictionary (without using multiple dictionaries), I went to the effort of exposing public properties for the various private members, as this is generally a good technique to use (if a little redundant, here).
- The dictionary now stores this attribute information against an ObjectId rather than the tag string. Roland made the excellent point that blocks can contain attributes with duplicate tags, so this is much safe. We also had to use the ObjectId of the AttributeReference, as later on inside the jig's Update() function we no longer have access to the AttributeDefinition.
Update 2:
Please see this more recent post for an implementation that works properly with multiline attributes, UCS and annotation scaling.