Database

April 30, 2014

Many thanks to Holger Rasch who worked out how to fix the code in this previous post. It really doesn’t matter that 3 years have passed, Holger – I have no doubt people will greatly appreciate the fact the code can now run without causing the annoying display issues it produced previously.

Holger made a few adjustments to the implementation to make sure the persistent hatch loop gets added and removed in the right places. Here’s the updated C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Colors;

using System;

namespace HatchJig

{

classJigUtils

{

// Custom ArcTangent method, as the Math.Atan

// doesn't handle specific cases

publicstaticdouble Atan(double y, double x)

{

if (x > 0)

returnMath.Atan(y / x);

elseif (x < 0)

returnMath.Atan(y / x) - Math.PI;

else// x == 0

{

if (y > 0)

returnMath.PI;

elseif (y < 0)

return -Math.PI;

else// if (y == 0) theta is undefined

return 0.0;

}

}

// Computes Angle between current direction

// (vector from last vertex to current vertex)

// and the last pline segment

publicstaticdouble ComputeAngle(

Point3d startPoint, Point3d endPoint,

Vector3d xdir, Matrix3d ucs

)

{

var v =

newVector3d(

(endPoint.X - startPoint.X) / 2,

(endPoint.Y - startPoint.Y) / 2,

(endPoint.Z - startPoint.Z) / 2

);

double cos = v.DotProduct(xdir);

double sin =

v.DotProduct(

Vector3d.ZAxis.TransformBy(ucs).CrossProduct(xdir)

);

return Atan(sin, cos);

}

}

publicclassHatchJig : DrawJig

{

Point3d _tempPoint;

bool _isArcSeg = false;

bool _isUndoing = false;

Matrix3d _ucs;

Plane _plane;

Polyline _pline = null;

Hatch _hat = null;

public HatchJig(

Matrix3d ucs, Plane plane, Polyline pl, Hatch hat

)

{

_ucs = ucs;

_plane = plane;

_pline = pl;

_hat = hat;

AddDummyVertex();

}

protectedoverridebool WorldDraw(

Autodesk.AutoCAD.GraphicsInterface.WorldDraw wd

)

{

// Update the dummy vertex to be our 3D point

// projected onto our plane

if (_isArcSeg)

{

var lastVertex =

_pline.GetPoint3dAt(_pline.NumberOfVertices - 2);

Vector3d refDir;

if (_pline.NumberOfVertices < 3)

refDir = newVector3d(1.0, 1.0, 0.0);

else

{

// Check bulge to see if last segment was an arc or a line

if (_pline.GetBulgeAt(_pline.NumberOfVertices - 3) != 0)

{

var arcSegment =

_pline.GetArcSegmentAt(_pline.NumberOfVertices - 3);

var tangent = arcSegment.GetTangent(lastVertex);

// Reference direction is the invert of the arc tangent

// at last vertex

refDir = tangent.Direction.MultiplyBy(-1.0);

}

else

{

var pt =

_pline.GetPoint3dAt(_pline.NumberOfVertices - 3);

refDir =

newVector3d(

lastVertex.X - pt.X,

lastVertex.Y - pt.Y,

lastVertex.Z - pt.Z

);

}

}

double angle =

JigUtils.ComputeAngle(

lastVertex, _tempPoint, refDir, _ucs

);

// Bulge is defined as tan of one fourth of included angle

// Need to double the angle since it represents the included

// angle of the arc

// So formula is: bulge = Tan(angle * 2 * 0.25)

double bulge = Math.Tan(angle * 0.5);

_pline.SetBulgeAt(_pline.NumberOfVertices - 2, bulge);

}

else

{

// Line mode. Need to remove last bulge if there was one

if (_pline.NumberOfVertices > 1)

_pline.SetBulgeAt(_pline.NumberOfVertices - 2, 0);

}

_pline.SetPointAt(

_pline.NumberOfVertices - 1, _tempPoint.Convert2d(_plane)

);

// Only when we have enough vertices

if (_pline.NumberOfVertices >2)

{

_pline.Closed = true;

AppendTheLoop();

}

if (!wd.RegenAbort)

{

wd.Geometry.Draw(_pline);

if (_pline.NumberOfVertices > 2)

{

_hat.EvaluateHatch(true);

if (!wd.RegenAbort)

wd.Geometry.Draw(_hat);

// Take it out, we will create a new one later

_hat.RemoveLoopAt(0);

}

}

returntrue;

}

protectedoverrideSamplerStatus Sampler(JigPrompts prompts)

{

var jigOpts = newJigPromptPointOptions();

jigOpts.UserInputControls =

(UserInputControls.NullResponseAccepted |

UserInputControls.NoNegativeResponseAccepted |

UserInputControls.GovernedByOrthoMode);

_isUndoing = false;

if (_pline.NumberOfVertices == 1)

{

// For the first vertex, just ask for the point

jigOpts.Message = "\nSpecify start point: ";

}

elseif (_pline.NumberOfVertices > 1)

{

string msgAndKwds =

(_isArcSeg ?

"\nSpecify endpoint of arc or [Line/Undo]: " :

"\nSpecify next point or [Arc/Undo]: "

);

string kwds = (_isArcSeg ? "Line Undo" : "Arc Undo");

jigOpts.SetMessageAndKeywords(msgAndKwds, kwds);

}

else

returnSamplerStatus.Cancel; // Should never happen

// Get the point itself

var res = prompts.AcquirePoint(jigOpts);

if (res.Status == PromptStatus.Keyword)

{

if (res.StringResult.ToUpper() == "ARC")

_isArcSeg = true;

elseif (res.StringResult.ToUpper() == "LINE")

_isArcSeg = false;

elseif (res.StringResult.ToUpper() == "UNDO")

_isUndoing = true;

returnSamplerStatus.OK;

}

elseif (res.Status == PromptStatus.OK)

{

// Check if it has changed or not (reduces flicker)

if (_tempPoint == res.Value)

returnSamplerStatus.NoChange;

else

{

_tempPoint = res.Value;

returnSamplerStatus.OK;

}

}

returnSamplerStatus.Cancel;

}

publicbool IsUndoing

{

get

{

return _isUndoing;

}

}

publicvoid AddDummyVertex()

{

// Create a new dummy vertex... can have any initial value

_pline.AddVertexAt(

_pline.NumberOfVertices, newPoint2d(0, 0), 0, 0, 0

);

}

publicbool RemoveLastVertex()

{

// If there's a single vertex, don't attempt to remove the

// vertex: would cause a degenerate geometry error

// Returning false will tell the calling function to

// abort the transaction

if (_pline.NumberOfVertices == 1)

returnfalse;

// Let's remove our dummy vertex

if (_pline.NumberOfVertices > 0)

_pline.RemoveVertexAt(_pline.NumberOfVertices - 1);

// And then check the type of the last segment

if (_pline.NumberOfVertices >= 2)

{

double blg = _pline.GetBulgeAt(_pline.NumberOfVertices - 2);

_isArcSeg = (blg != 0);

// We have to sync the hatch with the polyline

AppendTheLoop();

}

returntrue;

}

privatevoid AppendTheLoop()

{

var ids = newObjectIdCollection();

ids.Add(_pline.ObjectId);

_hat.Associative = true;

_hat.AppendLoop(HatchLoopTypes.Default, ids);

}

[CommandMethod("HATJIG")]

publicstaticvoid RunHatchJig()

{

var doc = Application.DocumentManager.MdiActiveDocument;

var db = doc.Database;

var ed = doc.Editor;

// Create a transaction, as we're jigging

// db-resident objects

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

{

var btr =

(BlockTableRecord)tr.GetObject(

db.CurrentSpaceId, OpenMode.ForWrite

);

var normal =

Vector3d.ZAxis.TransformBy(

ed.CurrentUserCoordinateSystem

);

// We will pass a plane to our jig, to help

// with UCS transformations

var plane = newPlane(Point3d.Origin, normal);

// We also pass a db-resident polyline

var pl = newPolyline();

pl.Normal = normal;

btr.AppendEntity(pl);

tr.AddNewlyCreatedDBObject(pl, true);

// And a db-resident hatch

var hat = newHatch();

// Use a non-solid hatch pattern, to aid jigging

hat.SetHatchPattern(

HatchPatternType.PreDefined,

"ANGLE"

);

// But let's make it transparent, for fun

// Alpha value is Truncate(255 * (100-n)/100)

hat.ColorIndex = 1;

hat.Transparency = newTransparency(127);

// Add the hatch to the modelspace & transaction

var hatId = btr.AppendEntity(hat);

tr.AddNewlyCreatedDBObject(hat, true);

// And finally pass everything to the jig

var jig =

newHatchJig(

ed.CurrentUserCoordinateSystem, plane, pl, hat

);

while (true)

{

var res = ed.Drag(jig);

switch (res.Status)

{

// New point was added, keep going

casePromptStatus.OK:

jig.AddDummyVertex();

break;

// Keyword was entered

casePromptStatus.Keyword:

if (jig.IsUndoing)

jig.RemoveLastVertex();

break;

// The jig completed successfully

casePromptStatus.None:

// You can remove this next line if you want

// the vertex being jigged to be included

if (jig.RemoveLastVertex())

{

tr.Commit();

}

return;

// User cancelled the command

default:

// No need to erase the polyline & hatch, as

// the transaction will simply be aborted

return;

}

}

}

}

}

}

Here’s the code in action, with none of the former visual artifacts:

Thanks again, Holger! :-)

Update:

After a comment from Robbo, I went ahead and made the generated hatch associative to its boundary by adding a single line of code (“hat.Associative = true;”) once the hatch has been made db-resident. You can, of course, comment out this line to revert to the previous behaviour.

Update 2:

Well that’s strange. It turns out Holger’s code was already setting boundary associativity… not sure how I missed that. Anyway, I have addressed an issue pointed out in the comments (thanks, aks!) where if you hit the spacebar immediately after starting the jig, an exception gets thrown. This has now been fixed in the above code.

May 27, 2011

Another Friday, another installment of Wayne Brill’s AutoCAD .NET training DevTV series (to complement those frompreviousweeks). Today’s session is focused on responding to database events and creating a palette-based user interface.

May 20, 2011

Another Friday, another installment of Wayne Brill’s AutoCAD .NET training DevTV series (to complement those from previousweeks). Today’s session is focused on the fundamentals of AutoCAD’s drawing database.

April 19, 2010

A big thanks to Stephen Preston for passing on this very interesting information. Stephen is one of the “volcanically challenged” members of my team, so fingers crossed he’ll be able to get a flight across to the UK in the coming days. Aside from Stephen’s delayed trip back to the motherland, the recent travel disruptions have also impacted an Inventor API training class being held in Moscow (which will go ahead with a back-up trainer) and extended another team member’s vacation in the Philippines.

The lack of a native 64-bit driver for ODBC, ADO, OLE DB, etc. access to Microsoft Access databases has been a big problem for developers porting their code to support 64-bit, especially as when working within an AutoCAD plugin you’re not able to thunk down to 32-bit using WoW64.

January 25, 2010

In the last post we looked at some code to define a block and insert it into the model-space of an AutoCAD drawing. Today we’re going to look at creating a group.

Before we dive into the code, it’s worth taking a quick look at the difference between a block and a group, to understand the differences between these two container/aggregator objects. I know that much of this information is available elsewhere, but hey – I’m on a roll, so I request your forbearance.

Let’s start with looking at the top-level structure of an AutoCAD Database (which is equivalent to a DWG file):

On the left of the above image we have a number of symbol tables. Symbol tables are the “traditional” container for data in an AutoCAD drawing, and have been in the format since the 80s (i.e. the dawn of time, from a PC technology perspective :-). Symbol tables store the names of objects within the objects themselves, which means you need to open the contained objects (symbol table records) to discover what they’re called.

The containers on the right, based on dictionaries (the DBDictionary class in .NET), are the more modern container inside DWGs. Dictionaries are like lookup tables: the entries – while sequentially accessible – are stored against a unique name, which means they’re more efficient if you need to identify a particular named entry.

Blocks are implemented via symbol tables, as they pre-date the introduction of dictionaries. Blocks are really about structuring content for repeated use, although – having said that – they’re also the mechanism used for external references, which are clearly not used repeatedly within a particular drawing.

Let’s take a look at the drawing structure we created in the last post: we created a new block definition called “Square” and then created a block reference in the model-space pointing to that block definition.

We can have multiple block references pointing to the same block definition, and if we change the contents of the block definition this will impact the various references.

Now let’s look at groups: as a more recently introduced mechanism, these are implemented using dictionaries. There’s a specific Group Dictionary that contains all the group objects in a drawing, and these, in turn, refer to sets of entities in the model-space (or paper-space, for that matter).

Groups collect entities together – and can be nested (but non-overlapping), just like blocks – but the individual elements retain the ability to be modified independently.

After groups were first implemented (back in R13, maybe? I forget…), I recall there were performance issues, and – in all honestly – I haven’t used them very heavily myself, over the years. I’d be curious to hear from this blog’s readership if anyone has experiences they wish to share: I would hope either that the initial performance problems have been resolved or that I’m misremembering and they work just fine for people.

Anyway, that’s it for the brief overview of blocks vs. groups: here’s the updated C# code that introduces a CG command to create a group.

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

namespace CollectionCreation

{

publicclassCommands

{

[CommandMethod("CB")]

publicvoid CreateBlock()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Database db = doc.Database;

Editor ed = doc.Editor;

Transaction tr =

db.TransactionManager.StartTransaction();

using (tr)

{

// Get the block table from the drawing

BlockTable bt =

(BlockTable)tr.GetObject(

db.BlockTableId,

OpenMode.ForRead

);

// Check the block name, to see whether it's

// already in use

PromptStringOptions pso =

newPromptStringOptions(

"\nEnter new block name: "

);

pso.AllowSpaces = true;

// A variable for the block's name

string blkName = "";

do

{

PromptResult pr = ed.GetString(pso);

// Just return if the user cancelled

// (will abort the transaction as we drop out of the using

// statement's scope)

if (pr.Status != PromptStatus.OK)

return;

try

{

// Validate the provided symbol table name

SymbolUtilityServices.ValidateSymbolName(

pr.StringResult,

false

);

// Only set the block name if it isn't in use

if (bt.Has(pr.StringResult))

ed.WriteMessage(

"\nA block with this name already exists."

);

else

blkName = pr.StringResult;

}

catch

{

// An exception has been thrown, indicating the

// name is invalid

ed.WriteMessage(

"\nInvalid block name."

);

}

} while (blkName == "");

// Create our new block table record...

BlockTableRecord btr = newBlockTableRecord();

// ... and set its properties

btr.Name = blkName;

// Add the new block to the block table

bt.UpgradeOpen();

ObjectId btrId = bt.Add(btr);

tr.AddNewlyCreatedDBObject(btr, true);

// Add some lines to the block to form a square

// (the entities belong directly to the block)

DBObjectCollection ents = SquareOfLines(5);

foreach (Entity ent in ents)

{

btr.AppendEntity(ent);

tr.AddNewlyCreatedDBObject(ent, true);

}

// Add a block reference to the model space

BlockTableRecord ms =

(BlockTableRecord)tr.GetObject(

bt[BlockTableRecord.ModelSpace],

OpenMode.ForWrite

);

BlockReference br =

newBlockReference(Point3d.Origin, btrId);

ms.AppendEntity(br);

tr.AddNewlyCreatedDBObject(br, true);

// Commit the transaction

tr.Commit();

// Report what we've done

ed.WriteMessage(

"\nCreated block named \"{0}\" containing {1} entities.",

blkName, ents.Count

);

}

}

[CommandMethod("CG")]

publicvoid CreateGroup()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Database db = doc.Database;

Editor ed = doc.Editor;

Transaction tr =

db.TransactionManager.StartTransaction();

using (tr)

{

// Get the group dictionary from the drawing

DBDictionary gd =

(DBDictionary)tr.GetObject(

db.GroupDictionaryId,

OpenMode.ForRead

);

// Check the group name, to see whether it's

// already in use

PromptStringOptions pso =

newPromptStringOptions(

"\nEnter new group name: "

);

pso.AllowSpaces = true;

// A variable for the group's name

string grpName = "";

do

{

PromptResult pr = ed.GetString(pso);

// Just return if the user cancelled

// (will abort the transaction as we drop out of the using

// statement's scope)

if (pr.Status != PromptStatus.OK)

return;

try

{

// Validate the provided symbol table name

SymbolUtilityServices.ValidateSymbolName(

pr.StringResult,

false

);

// Only set the block name if it isn't in use

if (gd.Contains(pr.StringResult))

ed.WriteMessage(

"\nA group with this name already exists."

);

else

grpName = pr.StringResult;

}

catch

{

// An exception has been thrown, indicating the

// name is invalid

ed.WriteMessage(

"\nInvalid group name."

);

}

} while (grpName == "");

// Create our new group...

Group grp = newGroup("Test group", true);

// Add the new group to the dictionary

gd.UpgradeOpen();

ObjectId grpId = gd.SetAt(grpName, grp);

tr.AddNewlyCreatedDBObject(grp, true);

// Open the model-space

BlockTable bt =

(BlockTable)tr.GetObject(

db.BlockTableId,

OpenMode.ForRead

);

BlockTableRecord ms =

(BlockTableRecord)tr.GetObject(

bt[BlockTableRecord.ModelSpace],

OpenMode.ForWrite

);

// Add some lines to the group to form a square

// (the entities belong to the model-space)

ObjectIdCollection ids = newObjectIdCollection();

DBObjectCollection ents = SquareOfLines(8);

foreach (Entity ent in ents)

{

ObjectId id = ms.AppendEntity(ent);

ids.Add(id);

tr.AddNewlyCreatedDBObject(ent, true);

}

grp.InsertAt(0, ids);

// Commit the transaction

tr.Commit();

// Report what we've done

ed.WriteMessage(

"\nCreated group named \"{0}\" containing {1} entities.",

grpName, ents.Count

);

}

}

privateDBObjectCollection SquareOfLines(double size)

{

// A function to generate a set of entities for our block

DBObjectCollection ents = newDBObjectCollection();

Point3d[] pts =

{ newPoint3d(-size, -size, 0),

newPoint3d(size, -size, 0),

newPoint3d(size, size, 0),

newPoint3d(-size, size, 0)

};

int max = pts.GetUpperBound(0);

for (int i = 0; i <= max; i++)

{

int j = (i == max ? 0 : i + 1);

Line ln = newLine(pts[i], pts[j]);

ents.Add(ln);

}

return ents;

}

}

}

The CG command allows us to create a group, just as the CB command created a block. We can even run them one after the other, using the same name (because one will use the name as a unique ID into the block table, the other into the group dictionary). And as the resultant squares are of different sizes, it should be obvious when the respective commands have done their work.