On previous class you saw that we can derive from AcDbObject to create powerful custom objects and store them inside NOD. On this class you will see that we can also create custom entities, deriving from AcDbEntity or one of its derived classes, which will present you a new way of thinking about how much powerful an ObjectARX application can be.
At the launch of AutoCAD 13 ObjectARX SDK opened a new world for developers and for Autodesk itself. Custom entities allows developers to build rich graphical applications and present several new features not included into native AutoCAD entities. Autodesk also start to develop other applications based on new entities that brought great functionalities and unique experience.
When you think about a custom entity you need to first think on how it will be used, handled, edited and all features it will present to users. This will allow you to outline the custom entity behavior and list all tasks it will need to support and perform. This step is very important to decide if is better to develop a custom entity or if is better to use a native AutoCAD entity adding some data using XData or XRecords.
In other hand, when you use standard AutoCAD entities you will need also to handle its behavior to support and manage all things users may perform with it. This is not quite simple and depending on how creative are your users it will take you much time. When you adopt the custom entity solution it will give you much advantage on handling user operations but it will be more complex to implement.
Advantages on using custom entities
Several advantages will point you to the custom entities approach. I would like to list some of these advantages just to allow you to perceive how powerful it is:
Custom graphics: When you create your own custom entity you are responsible for its graphics. ObjectARX provides you some primitive drawing functions that allow you to draw your custom entity using everything you need. No matter if your entity is too simple or too complex, ObjectARX provides tools to do that.
Custom entity graphics
The AcGi library provides all you need to construct your custom entity's graphics. Basically your custom entity will be presented on AutoCAD screen using one or both of the following methods:
AcDbEntity::worldDraw (AcGiWorldDraw * pWd);
virtual void
AcDbEntity::viewportDraw (AcGiViewportDraw * pVd);
The first function, called worldDraw(), is responsible to draw standard graphics for your entity. The second function, called viewportDraw(), is optional and it allows you to draw viewport dependent graphics. These functions receive a drawing context pointer that will allow you to perform your drawing as well.
These functions are called several times due several reasons and you need to provide the code inside them as faster and efficient as you can. Don't perform heavy calculations, long loops and other time consuming tasks there. If you are using native entities to draw you custom entity does not declare them inside these functions. Declare them as members of your class and just forward the calls inside worldDraw() or viewportDraw() to these embedded entities.
AutoCAD performs the drawing process walking through all database entities and calling first the worldDraw() method. If the worldDraw() method returns Adesk::kFalse, AutoCAD also walks through each viewport and call entity's viewportDraw() method. Exactly at this point you may draw a different graphic depending on each viewport configuration.
The provided drawing primitives are quite simple and I will just list them here: Circle, Circular arc, Polyline, Polygon, Mesh, Shell, Text, Xline and Ray. Please refer to the AcGi documentation inside ObjectARX SDK for instructions and detailed information on how to use them. Primitives are called using a geometry() function.
Custom entities allows you to also subdivide them. This feature is done using the AcGiSubEntityTraits object. The AcGiSubEntityTraits object sets graphical attribute values using the following traits functions:
- Color
- Layer
- Linetype
- Polygon fill type
- Selection marker
Adesk::Boolean MyEnt::worldDraw(AcGiWorldDraw *pW) {
AcGePoint3d verts[5];}
// Create some random points
verts[0] = verts[4] = AcGePoint3d(-0.5, -0.5, 0.0);
verts[1] = AcGePoint3d( 0.5, -0.5, 0.0);
verts[2] = AcGePoint3d( 0.5, 0.5, 0.0);
verts[3] = AcGePoint3d(-0.5, 0.5, 0.0);
// Set the subentity color as 3
pW->subEntityTraits().setColor(3);
// Draw the polyline primitive
pW->geometry().polyline(5, verts);
return Adesk::kTrue;
AcGePoint2d lleft, uright;
// Get viewport's DC coordinates
pV->viewport().getViewportDcCorners(lleft,uright);
// Perform some math here
double xsize = uright.x - lleft.x;
double ysize = uright.y - lleft.y;
xsize /= 10.0;
ysize /= 10.0;
double xcenter = uright.x - xsize;
double ycenter = uright.y - ysize;
double hxsize = xsize / 2.0;
double hysize = ysize / 2.0;
AcGePoint3d verts[5];
// Set vertex initial value
for (int i=0; i<5; i++) {
verts[i].x = xcenter;verts[i].y = ycenter;verts[i].z = 0.0;
verts[0].x -= hxsize;
verts[0].y += hysize;
verts[1].x += hxsize;
verts[1].y += hysize;
verts[2].x += hxsize;
verts[2].y -= hysize;
verts[3].x -= hxsize;
verts[3].y -= hysize;
verts[4] = verts[0];
// Set the subentity color as 3
pV->subEntityTraits().setColor(3);
// Draw the polyline on DC context
pV->geometry().polylineDc(5, verts);
Implementing Object Snap (OSNAP)
Your custom entity probably will need to provide some precision points through Object Snap feature. Depending on how much complex your custom entity is you will need to implement several OSNAP points and several types like EndPoint, Center, etc. To add OSNAP features to your custom entity you will need to add the following method to your class (there are other signatures):
virtual Acad::ErrorStatus AcDbEntity::getOsnapPoints(
AcDb::OsnapMode osnapMode,
int gsSelectionMark,
const AcGePoint3d& pickPoint,
const AcGePoint3d& lastPoint,
const AcGeMatrix3d& viewXform,
AcGePoint3dArray& snapPoints,
AcDbIntArray& geomIds) const;
This function will allow you to fill the passed in AcGePoint3dArray with those points that match with the provided osnapMode. Possible values to osnapMode are:
AcDb::kOsModeEnd: endpoint on the entity that is nearest to the pickPoint.
AcDb::kOsModeMid: midpoint (of any line, arc, etc., subentity) that is nearest to the pickPoint.
AcDb::kOsModeCen: center point (of any circle or arc subentity) that is nearest to the pickPoint.
AcDb::kOsModeNode: node point that is nearest to the pickPoint.
AcDb::kOsModeQuad: quad point that's nearest to pickPoint.
AcDb::kOsModeIns: insertion point of the entity (the insertion of a BlockReference or an MText object).
AcDb::kOsModePerp: intersection point of the entity and a line perpendicular to it passing through lastPoint.
AcDb::kOsModeTan: point on the entity where a line passing through lastPoint will be tangent to the entity
AcDb::kOsModeNear: Find the point on the entity that's nearest to pickPoint.
Imagine your custom entity is a rectangle and the user is running a LINE command and hover your entity with the EndPoint OSNAP enabled. Your entity will need to respond AutoCAD providing those points that could be used as EndPoints of your entity. In this case, inside your getOsnapPoints() function, you will need to fill the AcGePoint3dArray with the points of the rectangle corner. AutoCAD choose which of these points are inside the aperture box and the closest point to the cursor. So, your function will be something like:
Acad::ErrorStatus MyEnt::getOsnapPoints(
AcDb::OsnapMode osnapMode,
int gsSelectionMark,
const AcGePoint3d& pickPoint,
const AcGePoint3d& lastPoint,
const AcGeMatrix3d& viewXform,
AcGePoint3dArray& snapPoints,
AcDbIntArray& geomIds) const {
assertReadEnabled();
switch (osnapMode) {case AcDb::kOsModeEnd:
snapPoints.append(pt[0]);
snapPoints.append(pt[1]);
snapPoints.append(pt[2]);
snapPoints.append(pt[3]);
break;
}
return Acad::eOk;
}
The intersection OSNAP is not implemented through getOsnapPoints() method. As it is much more complex there is a special function called intersectWith() to handle that. I won't present details about this here but you may read and find further information inside ObjectARX SDK documentation.
Implementing GRIP and Stretch points
Grip points provides a great and simple way to user edit and transform entities. You will probably want to implement this feature for your entity. Further, stretch points will allow users to stretch your entity as well. These two features are very simple to implement and make your entity much more flexible and powerful.
Basically you just need to inform which are your key points to Grip and stretch. Other functions will be responsible on set the behavior of your entity when each Grip and stretch point are used. Depending on the complexity of your entity these functions may become a little bit complex.
For the Grip points feature you will need to implement a couple of functions. The first function, called getGripPoints(), will return those points where you would like to enable a Grip. The second function, called moveGripPointsAt() will perform the action when Grips are fired:
virtual Acad::ErrorStatus
AcDbEntity::getGripPoints (AcGePoint3dArray& gripPoints,
AcDbIntArray& osnapModes,AcDbIntArray& geomIds) const;
virtual Acad::ErrorStatus
AcDbEntity::moveGripPointsAt (const AcDbIntArray& indices,
const AcGeVector3d& offset);
Remember that Grip points don't need to be created only where there is a part of your graphics. You can, for instance, create a Grip at the center of a rectangle and there is nothing drawn there.
The getGripPoints() function receive 3 arguments. Currently only the first argument is used. It is an array of 3D points passed in by AutoCAD. This array contains all points involved into the current Grip operation. As other entities may be involved into this operation this array may already be filled. You just will append your desired points to this array. The points you have appended inside getGripPoints() will be identified by an index from the order they were appended to the point array.
The moveGripPointsAt() function will receive the index array and a 3D vector sent by AutoCAD with the current transformation (AcGeVector3d) being applied. At this time you just need to loop the index array, get each of its values (the index) and, depending on this value fire the transformation at the desired point. Imagine again your entity is a rectangle and you have 5 Grip points, one of each corner and one at the center. For each corner you will apply only the transformation to its grip and for the center grip you will apply the transformation to all of your points. The corner grip operation will result into a stretch at that corner and the center grip operation will result into a move of the whole entity. To apply the transformation to each point just call the transformBy() method passing in the AcGeVector3d received from AutoCAD.
In other hand, stretch points are defined and controlled by another two functions. They are much like the Grip functions and sometimes you just return a call to the Grip functions inside respective Stretch functions:
virtual Acad::ErrorStatus
AcDbEntity::getStretchPoints(
AcGePoint3dArray& stretchPoints) const;
virtual Acad::ErrorStatus
AcDbEntity::moveStretchPointsAt(
const AcDbIntArray& indices, const AcGeVector3d& offset);
The behavior of stretch functions are almost the same as the Grip functions and if your stretch points will behavior like Grip points you may just forward the call as below:
Acad::ErrorStatus MyEnt::getStretchPoints(
AcGePoint3dArray& stretchPoints) const {
AcDbIntArray osnapModes,geomIds;
return MyEnt::getGripPoints(stretchPoints,osnapModes,geomIds);
}
Acad::ErrorStatus MyEnt::moveStretchPointsAt(
const AcDbIntArray& indices, const AcGeVector3d& offset) {
}
The same concept of worldDraw() and viewportDraw() functions also applies here to the moveGripPointsAt() and moveStretchPointsAt() functions. They are called several times and they need to be as faster as you can. When you click at an entity's grip the moveGripPointsAt() function is called for every small mouse movement.
Implementing Transformation
Your custom entity needs to support transformation if you would like to allow users to perform commands like MOVE, ROTATE and SCALE. This feature is implemented through transformBy() method which receives a transformation matrix representing the current transformation being applied to your entity. Inside this function you will apply this matrix to your entity's data to reflect the modifications. The AcGeMatrix3d class support all types of transformations and encapsulate them through a matrix:
virtual Acad::ErrorStatus
AcDbEntity::transformBy(const AcGeMatrix3d& xform);
A typical implementation of transformBy() function could be:
Acad::ErrorStatus MyEnt::transformBy(
const AcGeMatrix3d& xform) {
pt[0].transformBy(xform);
pt[1].transformBy(xform);
pt[2].transformBy(xform);
pt[3].transformBy(xform);
}
In some special cases that I won't present here, you may need to apply the transformation to a clone or copy of your original entity. This is done using the getTransformedCopy() method which receives the transformation matrix and a pointer to be filled with the entity's transformed copy.
Too much information? Next class I will present a short and practical example with a custom entity. Stay tuned!