Developing Add-Ons for CC3+ – Part 2: Entity Basics

This is the second article in my series about XP development. To understand this article properly, you should be familiar with the contents of the previous articles.

In this article, we’ll talk about how we can manipulate our drawing. In CC3+, a drawing is really a series of entities, so we are going to have a closer look at what an entity really is, how to create new entities in the map, and how to access and manipulate existing entities.

Entities

Everything in a CC3+ map is an entity; a symbol, a line, a landmass and so on. This term should be well known to all CC3+ mappers, as it is the term used in official documentation. However, it isn’t just these visible things that are entities, almost everything stored in a CC3+ drawing is an entity, such as a map note or an effect. We can view an entity as a data container for one specific thing or aspect of our map.

When working on an XP, you are almost always going to be handling entities. After all, manipulating entities is needed no matter what you want to do with the drawing, including extracting information from it, so understanding how to work with these is very important.

Many different types of entities exists, for example a symbol definition entity is very different from a map note entity. The most tricky part about entities are handling them correctly. Every entity type is represented by a different type (a struct) in code, but when you request an entity from CC3+, you get returned an entity record, which is a pointer to an entity, and this entity record is defined as a Union type, or more specifically a discriminated union (If you unfamiliar with union, you should read the linked articles). So, before we can do anything with it, we need to determine the correct type for it. Trying to treat an effect as a symbol will have very unpredictable results, most likely leading to a crash.

Fortunately, all the different entity types all have some common properties as their first member (CStuff), so we can use this data to determine which entity type we are working with.

You can find the different entity types supported by CC3+ in the _ESTRUC.H and XT_Entities.h files in the toolkit.

Drawing List

Each CC3+ drawing contains a whole lot of entities. These are all stored in the drawing list in the drawing. This is a doubly linked list of entity records. Each entity record in this list holds an entity, and optionally a sublist of entities. Thus the drawing list hierarchy ends up being more similar to a tree than a simple list. This list is also referred as the drawing database.

Whenever the screen is updated (due to redraw, zoom, pan, etc), CC3+ will iterate through the drawing list and draw the entities found in it on the screen, one at a time.

Technically, more than one drawing list can be loaded, but we’ll usually have to consider only one, although this list WILL contain multiple sublists. A typical scenario for where sublists are used is in symbol definitions. A symbol definition is an entity type in itself, so it appears in the main drawing list. But a symbol is also made up of multiple component entities (lines, images, control points and more), and all of these appear in the sublist of the symbol definition. Symbol references with attributes also use sublists for storing these attributes.

Note that as an XP developer, you will only be concerning yourself about the entity records themselves, CC3+ will handle all the pointer manipulation behind the scenes.

 

Adding Entities

Before we dig into the workings of the drawing list, let us experiment with adding a few simple entities. For this, we’ll obviously have to start a new XP Project in visual studio as we learned in Part 1, or you can continue working on the same project. To provide cleaner examples, I’ll start from scratch however.

A Simple Line

Let us start easy with just adding a simple line to the drawing. This will be very similar to the standard line command in CC3+, although we’ll stick with just drawing a single segment before ending the command. When the command is used, the line will be drawn using the current properties set in the status bar.

The code below is everything that is needed for this. See below for a line-by line explanation of this code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void XPCALL XP2Line(void);
void XPCALL XP2LineP1 (int Result,int Result2,int Result3);
void XPCALL XP2LineP2 (int Result,int Result2,int Result3);


char CList[] = "XP2LINE\0\0";
PCMDPROC PList[] = { About, XP2Line };


LINE2 XPLine = { sizeof(LINE2), ET_LINE2, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0.0, 0,
				0.0, 0.0, 1.0, 1.0 };

FORMST(lpszStart, "Line Start:\0")
FORMST(lpszEnd, "Line End:\0")

RDATA P1Req =
{ sizeof(RDATA), RD_2DC, NULL, RDF_C, (DWORD*)& XPLine.Line.p1,
  (DWORD*)& lpszStart,RDC_XH, XP2LineP1, NULL, NULL, 0, NULL, 0 };

RDATA P2Req =
{ sizeof(RDATA), RD_2DC, NULL, RDF_C, (DWORD*)& XPLine.Line.p2,
  (DWORD*)& lpszEnd,RDC_RBAND, XP2LineP2, NULL, NULL, 0, NULL, 0 };

void XPCALL XP2Line() {

	ReqData(&P1Req);

}

void XPCALL XP2LineP1(int Result, int Result2, int Result3) {
	if (Result != X_OK) { CmdEnd(); return; }
	NewCsrOrg(XPLine.Line.p1.x, XPLine.Line.p1.y);
	ReqData(&P2Req);
}

void XPCALL XP2LineP2(int Result, int Result2, int Result3) {

	if (Result != X_OK) { CmdEnd(); return; }
	GetCStuff(&XPLine.CStuff);
	MarkUndoAdd();
	pENTREC pEntRec = DLApnd(NULL, (pENTREC)& XPLine);
	EDraw(pEntRec);

	CmdEnd();
}

 

Code Explanation

Line 1-3: Standard C++ function declarations for the functions we will be using for our command. These three lines would commonly go to the header file (main.h if you are using my custom VS template), or at the top of the source code file (main.cpp in my template).

Line 6-7: As you should remember from part 1, these lines adds new commands to CC3+. The first line contains a zero-terminated list of commands this XP adds, while the second is the list of functions these commands will call. Remember that the first entry in the function list is always the about command, which is not represented in the command list, so here we add one new command, XP2LINE, which maps to the XP2Line function.

Line 10-11: This line creates a new instance of the LINE2 struct, the type responsible for basic lines. Right now, it is being set up with some default settings for layer, fill, width and other properties, we’ll populate it with the correct ones later. Pay especially attention to the two first parameters. The first specifies the size of the entity, and the second specifies the entity type. Both of these might seems slightly odd, since we already declared this to be of type LINE2. Why isn’t the type good enough by itself? The secret here lies in the drawing list. CC3+ will be accessing entities here without knowing their exact type, so the size is required for proper memory management and pointer manipulation, while storing the entity type is the only way we can figure out what entity type this really is when retrieving it from the drawing list.

Line 13-14: FormSt is the text formatting and output services in CC3+. A more proper explanation is the topic for another day, but what these two lines do is to prepare text for later display by CC3+, such as on the command line. The prepared text is stored in the lpszStart and lpszEnd variables.

Line 16-20: RDATA is a packet of information to describe a data request by CC3+. Again, this is a topic for later, but what both of these data requests do is to ask for a point. Note that these lines only defines the request, the actual requesting is done later.

  • RD_2DC tells CC3+ that we want to request a xy coordinate
  • (DWORD*)& XPLine.Line.p1 is the address of the first node in our new line instance. This is where the result of the request will be stored
  • (DWORD*)& lpszStart is the address of the FormSt packet to use as the prompt.
  • RDC_XH indicates that we will be using a crosshair cursor during the request
  • RDC_RBAND indicates the use of a “rubberband” cursor. A rubberband is anchored in one point while the other end follows our mouse cursor. This is an easy way of previewing the line.
  • XP2LineP1 is the function called when the data is received. This function is responsible for handling any errors and further work.

Line 24-28: This is the XP2Line function, which is called when we execute the XP2LINE command in CC3+

Line 26: The ReqData calls causes CC3+ to request data using an already defined request. In this case we request data via the request ReqP1 which asks for the first point, and once the data have been collected, hands over control to the the function defined in the request, in this case XP2LineP1. Note that the data request can either be fulfilled by clicking with your mouse in the drawing window, or typing the coordinates on the command line. The latter means that even if the command can be completed graphically, it also works perfectly for a macro command.

Line 30-34: This is the function that will be called as a result of the data request we did on line 26. Note that the actual data was put directly into our XPLine variable, but we’ll want to check the Result parameter to see if the data we requested was retrieved successfully.

Line 31: Here we check if the data was retrieved successfully or not. Here we just end the command immediately if the data provided was not ok. Note that we need to call CmdEnd to have the command terminate properly. The expected values for Result is

  • X_OK: Everything fine, user entered data, and the data was in the correct format
  • X_DFLT: The user accepted the default option. Assuming you actually provided a default option for the command, this is also an OK state
  • X_CAN: The user canceled the command
  • X_BAD: The data received did not fit the format requested (i.e. for this command it was something else than an xy-coordinate)

Line 32: This line sets up the origin point for the cursor. Remember from above that the second request used a rubberband cursor? This line sets the origin of this rubberband to the location of the first point of our line, allowing the rubberband to correctly represent a preview of the location of our line as you move the cursor around to place the second point.

Line 33: Similar to line 26, we now issue a data request for the user to provide the end point of the new line

Line 36-45: Similar to the previous function, this function handles the data from the second request (line 33)

Line 38: Same as line 31

Line 39: The GetCStuff function retrieves the common properties, like color, width, layer, etc from the current settings (as indicated on the status line) and apply it to the CStuff part of our new entity. This overwrites all the defaults we set up on line 10, and is why we didn’t care too much about it then.

Line 40: We are now about to make a modification to our drawing, something we potentially wish to undo later. The MarkUndoAdd function sets an undo point at this location, which means that any drawing changes we do after this can be undone.

Line 41: DLApnd appends an entity to a drawing list. To add something to the default drawing list, we just specify NULL as the first parameter. The second parameter to this function is obviously the entity to add. CC3+ will return the entity record of the entity we just added.

Line 42: We tell CC3+ to go ahead and update the screen with the new entity. If we don’t call this, the entity will still be added, but the screen won’t be updated, so we won’t see the entity until a redraw is issued.

Line 44: Finally, we tell CC3+ that the current command is finished. If we forget this, it can cause problems for the next command, and will often lead to CC3+ crashing on exit instead of exiting cleanly.

A Pentagram

So, let us build on what we did earlier, and make a command that draws a nice pentagram in a circle for us, just by specifying the start and end point of the first leg of the star (two clicks, just as with the line above). This command will require some math, as well as the generation of multiple entities instead of just one. Do note that we are going to draw several distinct entities here, drawing a lot of entities using a single command still results in a lot of entities in the map, CC3+ won’t let you manipulate them like a single entity (but you can always group it). Treating multiple entities as a singular entity is really what symbols are all about, a topic for another day.

Again, the description of the code is below the code. I’ll only explain the new stuff this time, much of the code is almost identical to what I explained above for the line command.

Note

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

74
75
76
77


78
79
80
81
82
83
84
85
86
87
88
void XPCALL XP2Pent(void);
void XPCALL XP2PentP1 (int Result,int Result2,int Result3);
void XPCALL XP2PentP2 (int Result,int Result2,int Result3);


char CList[] = "XP2LINE\0XP2PENT\0\0";
PCMDPROC PList[] = { About, XP2Line, XP2Pent };


GPOINT2 p1;
GPOINT2 p2;

FORMST(lpszCenter, "Center of Pentagram:\0")
FORMST(lpszEdge, "Edge of Pentagram:\0")

RDATA Pent1Req =
{ sizeof(RDATA), RD_2DC, NULL, RDF_C, (DWORD*)& p1,
  (DWORD*)& lpszCenter,RDC_XH, XP2PentP1, NULL, NULL, 0, NULL, 0 };

RDATA Pent2Req =
{ sizeof(RDATA), RD_2DC, NULL, RDF_C, (DWORD*)& p2,
  (DWORD*)& lpszEdge,RDC_RBAND, XP2PentP2, NULL, NULL, 0, NULL, 0 };

void XPCALL XP2Pent() {
	
	ReqData(&Pent1Req);

}


void XPCALL XP2PentP1(int Result, int Result2, int Result3) {
	
	if (Result != X_OK) { CmdEnd(); return; }
	NewCsrOrg(p1.x, p1.y);
	ReqData(&Pent2Req);

}

void XPCALL XP2PentP2(int Result, int Result2, int Result3) {

	if (Result != X_OK) { CmdEnd(); return; }
	MarkUndoAdd();

	LINE2 l = {{sizeof(LINE2), ET_LINE2, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0.0, 0},
			0.0, 0.0, 1.0, 1.0 };
	GetCStuff(&l.CStuff);

	l.Line.p1 = p1;
	l.Line.p2 = p2;

	float x = p1.x;
	float y = p1.y;
	
	pENTREC pEntRec = DLApnd(NULL, (pENTREC)& l);
	EDraw(pEntRec);

	for(int i = 0; i < 4; i++) {
		
		pEntRec = DLCopy(NULL, pEntRec);
		GetCStuff(pEntRec);
		CTMI2();
		CTMT2(-l.Line.p1.x,-l.Line.p1.y);
		CTMR2(144.0*M_PI/180);
		CTMT2(l.Line.p2.x,l.Line.p2.y);

		ETran(pEntRec);
		EDraw(pEntRec);

		x += pEntRec->Line.Line.p1.x;
		y += pEntRec->Line.Line.p1.y;
		
	}

	CIR2 c = {	{sizeof(CIR2), ET_CIR2, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0.0, 0},
	0.0, 0.0, 1.0, };
	GetCStuff(&c.CStuff);
	c.Circle.Center.x = x/5;
	c.Circle.Center.y = y/5;
	c.Circle.Radius = 
	  (float)(sqrt (pow((double)p2.x-(double)pEntRec->Line.Line.p1.x,2) + 
	  pow((double)p2.y-(double)pEntRec->Line.Line.p1.y,2)) / sqrt((5 - sqrt(5))/2));
	
	if(c.CStuff.LWidth == 0.0f) {
		c.CStuff.EFStyle = 0;
	}

	pEntRec = DLApnd(NULL, (pENTREC)& c);
	EDraw(pEntRec);

	CmdEnd();

}

 

Code Explanation

Line 1-3: Standard C++ function declarations.

Line 6-7: Adding the commands to CC3+.

Line 10-11: Declaring two points to hold the coordinates the clicks to define the size.

Line 13-14: FormSt packages for the prompts.

Line 16-20: RDATA packages for the requests.

Line 24-28: Main method for the XP2PENT command.

Line 31-37: Function to handle the first point.

Line 39-88: This function handles the reception of the second point, and draws our pentagram.

Line 44: We’ll be drawing 5 lines to make up the star, so we start by initializing a default line entity like we did with the XP2LINE command.

Line 47-48: Here we set the starting and ending nodes of the first line equal to the points we collected earlier.

Line 50-51: We’ll need to calculate the center of the pentagram. One simple way of doing this is to just add together all the points (handling x and y individually) and then dividing by 5. We start off here by declaring the variables, and assigning the values from our starting point.

Line 53-54: Append the initial line to the drawing list and draw it on screen.

Line 56-71: This loop runs once for each additional line we need to add. The general operation of this loop is to add another line identical to the previous line added, and then move and rotate it. This is easier than trying to calculate the points for the lines, although that is also a possibility.

Line 58: Here we add another line to the drawing list. We use DLCopy to add a copy of the previous entity to the drawing list.

Line 59: While most of the properties are already set since we just made a copy of the previous line, we still need to call GetCStuff again, otherwise the new copy will have the same entity ID as the original, which we do not want

Line 61-64: Here we set up the transformation matrix needed to rotate and move the entity. CTMI2 initializes the matrix, then we use a move translation (CTMT2) to move the line so that the first node is in the origin (0,0). We need to do this because all rotation is around the origin, so if we attempt to simply rotate it, the entity would end up in a completely different position of the screen. This move is accomplished by simply applying the negative value of the current coordinates of the node. The next line is the rotate translation (CTMR2). The angle in a pentagram is 36 degrees, but because we are basically “folding” the line, we need to start with 180 degrees which is a straight line and subtract the 36 degrees, which results in 144 degrees. Furthermore, the function expects the rotation to be in radians, so we multiply by Pi and divide by 180 to convert the 144 degrees to radians. Finally, we move the line so that the start node of the new line ends up in the same spot as the end node of the previous line.

This is just an initial glance into the topic on entity transformations. More on that in a future article.

Line 65: Here we apply the transformations we defined above to our new line.

Line 66: Draw the new line on screen.

Line 68-69: Grabs the coordinates of the nodes and add them to our x and y (see lines 50-51).

Line 73: Here we initialize a circle entity, to be used for the circle around the star.

Line 75-76: With the sum of the line nodes collected, we divide by 5 to get the center point of our star, which we then use as the center for the circle.

Line 77: Here we use the formula for a circle around a pentagon (which shares some properties with a pentagram, including the radius of the enclosing circle) to calculate the radius we need for the circle. In addition to a bit of math, there are also a few unnecessary casts in there, simply because explicitly declaring the casts quiets the compiler warnings (basically telling the compiler that yes, I know what I am doing here).

Line 79-81: You may remember that if a closed shape (like a circle) in CC3+ has a line width of 0, it appears as a completely filled shape, but if the line width is greater than zero, it will just have a filled outline of the specified width instead. These three lines checks if the line width is zero, and if it is, changes the fill style to hollow instead. If we didn’t do this, then the circle would completely cover the rest of the pentagram, making it appear as just a plain circle instead, which is not what we want.

Line 83: Add the circle to the drawing list.

Line 84: Draw the circle on screen.

Line 86: Always remember to end the command cleanly.

Map Note and a Lesson in Memory Management

Not all entities in CC3+ are visible things in the map. Things like map notes are also entities, and stored in the drawing list. Here is a simple example that will ask the user to input the name and content on the keyboard, and then create map note using that data. This example also shows how to handle multiple lines of input, like the MSGBOX command does.

Now, before we get to the code, we need to have a look at some memory management in CC3+. Let us look at the entity definition for a map note


typedef struct
{
	CSTUFF CStuff;

	unsigned char Version;
	unsigned char flags;

	char NName[32];

	char NData[1];
} NOTE;

The struct is pretty simple, it starts with the CStuff every other entity also starts with, then variables to hold a version number and flags, before it contains a variable for note name (NName) and a variable to hold the note content (NData). Note name is pretty straight forward, it is a 32 character string. CC3+ do expect these strings to be properly terminated with the NUL character though, so the note title itself can at most be 31 characters since the last position is occupied by the NUL character.

But the variable that is supposed to hold the actual content may look a bit strange. What kind of note can we make with only one character (not to mention this string too needs a NUL-character at the end.) This is where the memory management comes in. A map note can contain up to 8192 characters. But if we reserve 8192 characters in the struct, then every note entity will take up 8K of memory. Maybe not too much today, since we are usually not going to have thousands of notes, but the underlying FastCAD engine is written to be very efficient, and ensuring that the program doesn’t use more memory than is required is part of this. And to do this, it takes full advantage of C++ features (Technically, FastCAD is written in Assembly and C, but since we are working in C++, and those features are still present, I’m just going to refer to them as simply C++ features for the sake of brevity). One of the features (and a source of a lot of bugs if you don’t know exactly what you are doing) of C++ is that arrays bounds aren’t enforced. For example, in C++ you can define an array with 10 elements, and then immediately start accessing the element at index 25. In C#, Java and most other programming language, doing something like that would lead to an Array Out of Bounds exception (or similar) being thrown, but in C++, that is perfectly legal code. Of course, since we only made an array with 10 elements, C++ sees any memory beyond those first 10 elements as free, and to be used for other purposes. So, to be able to use this safely, we need to actually manually reserve that memory for our own use, and we also need to ensure it is freed when we are done with it. This does place an additional burden on us as the programmer to do things correctly, but it also allows us to do things not allowed in other languages.

In the code below, we use a vector object to perform the memory management for us. We’re not actually using the vector at all, we just create it with the correct size, since that will reserve the appropriate amount of memory for us, and then we immediately cast it to an entity record. This ensures that we have an entity record with enough reserved memory for the note we are making. The vector is allocated on the stack, so the memory will be freed automatically at the end of the current block, thus we ensure that we don’t have memory leaks. You could also use calls like malloc and free to allocate and free memory for you, which would accomplish the same result.

Now, here is the code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
void XPCALL XP2Note(void);
void XPCALL XP2NoteTitle (int Result,int Result2,int Result3);
void XPCALL XP2NoteContent (int Result,int Result2,int Result3);


char CList[] = "XP2LINE\0XP2PENT\0XP2NOTE\0\0";
PCMDPROC PList[] = { About, XP2Line, XP2Pent, XP2Note };


char noteTitle[32];
char cmdLineBuffer[257];
char noteContent[8500];

FORMST(lpszTitle, "Note Title:\0")
FORMST(lpszContent, "Note Content:\0")

RDATA Note1Req =
{ sizeof(RDATA), RD_TxWord, NULL, RDF_C+RDF_SPACEOK, (DWORD*)& cmdLineBuffer,
  (DWORD*)& lpszTitle,RDC_NONE, XP2NoteTitle, NULL, NULL, 0, NULL, 0 };

RDATA Note2Req =
{ sizeof(RDATA), RD_TxWord, NULL, RDF_C+RDF_SPACEOK, (DWORD*)& cmdLineBuffer,
  (DWORD*)& lpszContent,RDC_NONE, XP2NoteContent, NULL, NULL, 0, NULL, 0 };


void XPCALL XP2Note(void) {
	
	ReqData(&Note1Req);

}

void XPCALL XP2NoteTitle (int Result,int Result2,int Result3) {
	
	if (Result != X_OK) { CmdEnd(); return; }

	cmdLineBuffer[31] = '\0';
	strcpy_s(noteTitle,cmdLineBuffer);

	ReqData(&Note2Req);

}

void XPCALL XP2NoteContent (int Result,int Result2,int Result3) {

	if (Result == X_OK) {

		if(strlen(noteContent) >= 1 ) {
			std::string buffer = std::string(noteContent) + 
				"\r\n" + std::string(cmdLineBuffer);
			strcpy_s(noteContent,buffer.c_str());
		} else {
			strcpy_s(noteContent,cmdLineBuffer);
		}

		if(strlen(noteContent) < 8192) {
			ReqData(&Note2Req);
		} else {
			XP2NoteContent(X_DFLT,0,0);
		}

	} else if (Result == X_DFLT) {

		noteContent[8191] = '\0';
		
		const auto len = static_cast<int>(strlen(noteContent) + sizeof(NOTE));
		std::vector<char> buffer(len);
		auto p = reinterpret_cast<pENTREC>(buffer.data());

		p->CStuff = { len, ET_NOTE, 0, 0 , 0, 0, 0, 0, 0, 0, 0, 0, 0 };

		strncpy_s(p->Note.NName,noteTitle,32);

		#pragma warning(disable:4996)
		strncpy(p->Note.NData,noteContent,min(strlen(noteContent),8192));


		GetCStuff(p);

		DLApnd(NULL, p);
		CmdEnd();

	} else {
		
		CmdEnd();
	}

}

 

Code Explanation

Line 1-3: Standard C++ function declarations.

Line 6-7: Adding the commands to CC3+.

Line 10-12: Variables to hold input. CC3+ only allows the map note to be 31 characters (+ the NUL-terminator) so we set the title up with a size of 32. The command line can read at most 256 characters at a time, so we declare this buffer to be 257 characters to account for the NUL-character at the end. Finally, the note itself can have 8192 characters, but we need a bit more space, because the command line buffer will be added to the end here before we check the size and truncate, so it needs to be large enough to account for that.

Line 14-15: FormSt packages for the prompts.

Line 17-23: RDATA packages for the requests.

Line 26-30: Main method for the XP2NOTE command. Don’t do anything except immediately request the note title

Line 32-41: Handler for the callback from the title request.

Line 34: Aborts if no data received.

Line 36-37: Copy the command line buffer to the note title. Note that we start by setting the 32nd character to the NUL character, because the call to strcpy_s below is only going to copy the string up to the NUL-terminator. By setting it manually, we ensure that we don’t overflow the buffer by truncating the title to the required 31 characters, and if the title is already smaller than 31 characters, then this extra NUL at the last position won’t change anything because there will already be a NUL earlier in the string at the end of the title.

Line 39: Request the first line of content

Line 43-87: Handler method for the content callback

Line 45-60: This code block is called if data is received (Result == X_OK)

Line 47-53: Here we do a quick check if there is already some data in the content(noteContent) (i.e. if this is the first line or not). The reason for this is that we wish to put linebreaks between lines, and we don’t want a linebreak before the first line. If data already exists, we combine this with the new data received (cmdLineBuffer) and then we copy the result back to the content variable. If it is the first line, we simply copy the data we got straight to the content variable.

Line 55-59: Since a note cannot be more than 8192 characters, we now check the final length of the data, and if it is less than 8192 characters, we request another line. If it is already at 8192 characters and above, we simply call the current method with the result set to X_DFLT (se below), since this will cause the command to finish up instead of asking for more lines when we are already above the max size. If the size is smaller, we simply repeat the Note2Req data request, which will end up calling this method again recursively so we can process the next line of input.

Be aware that this choice is user-friendly, but it is not macro friendly since any macro who tries to provide more data than possible will fail with various error messages since the text strings we try to supply to the command will suddenly be treated as commands instead of input. Now, since the command line is at most 256 characters, it is very unlikely that someone will provide 32 full lines of input embedded in a macro, but technically, someone could do that. If you want to secure the command from those kind of extreme situations, instead of forcing the command to finish when over the limit, one could just continue to accept the input, but not copy the overflow to the content buffer, and simply continue processing until the macro terminates properly by sending a blank line.

Line 61-81: Here we handle the completion of the command. This block of code is triggered when Result is equal to X_DFLT, which happens if the user provides a blank line of input, OR click the right mouse button. When this happens, we stop requesting lines of data, and go ahead with creating the note entity.

Line 63: As we did earlier with the title, we want to ensure that we do not go beyond 8192 characters for the content, and that the last character is the NUL character. Setting this now ensures that we can calculate the size correctly below.

Line 65: Here we calculate the size of the entire entity record. This is very important for memory management purposes, you may remember our discussion earlier about memory management. The size of the entity record will be the size of the NOTE entity type itself (which includes the size occupied by the CStuff and the title) plus the size of the data for the note content.

Line 66: Here we allocate a buffer large enough to hold the entire entity record. We declare this as a char vector because a vector allows us to declare the exact size we need. Note that we simply do this to reserve the required area of memory, we are actually not using the vector itself for anything at all, it is just a convenient class that will allocate exactly the size of the memory we need. Additionally, since this allocated on the stack, the memory will be freed immediately upon the current code block ending.

Line 67: And here we cast the vector (or more precisely, the memory area reserved to hold the vector data) to an entity record (pENTREC).

Line 69: Populate some CStuff data, the important here is the entity length, as having this correct is vital for the entity record to work correctly when inserted into the drawing list, as well as the entity type (ET_NOTE)

Line 71: We copy the text from the title buffer into the note title. strncpy_s copies all characters up to and including the NUL terminator, but at most 32 characters.

Line 73-74: We now copy the data from the content buffer to the NData variable in the entity. However, you may note that we are using strncpy instead of strncpy_s. That latter is the safe version of this command, which ensures we don’t do dangerous things like copying beyond the bounds of the array. However, if you have been keeping attention so far, you’ll remember that this is exactly what we need to do. So, we’ll use the old non-safe version of this command. Visual Studio don’t like this, which is why we suppress the warning, otherwise the code won’t compile. We’ll also make sure that we don’t copy in too much data, so we tell the command to copy either the actual number of characters in the string, or 8192 (note max size), whichever is less.

Line 77: Gets the current properties and populates CStuff. The most important reason for using this call for a note entity is to generate an entity id for this note, all the other properties like fill and such is meaningless for a note.

Line 79: Appends the new entity record to the drawing list. Note that this copies the data from the entity record we just made, which is why it is important that the allocated memory for our working entity is freed properly.

Line 80: As usual, we end the command properly.

Line 82-84: If the result code indicates anything but X_OK (Leading us to process the line and ask for another) or X_DFLT (Leading us to take the data we have collected so far and finish up) then we simply exit the command cleanly instead of trying to construct the note. This situation can happen if the user hit the Esc key to abort the command.

Walking the Drawing List

Now that we know how to create entities and add them to the drawing, it is also important that we know how to handle entities already in there. And to do this, we need to start accessing the drawing list. We have already learned about the DLApnd function that allows you to add an entity to the drawing list, but there is a whole host of other functions too, such as DLScan which is used to scan the drawing list for entities, DLCopy that copies an entity and DLDelete that removes an entity from the drawing list. For a complete list, check out _DLMGR.H.

Commonly, we will start with a DLScan call, as this walks the drawing list and returns the entities one by one by using a callback method. In the callback, we typically check if the entity is of the correct type, and then perform our manipulation of it. The type of an entity is always available through the CStuff member, so if the pointer to the entity record is p, then we can always check the entity type in p->CStuff.EType. Once the type is determined, we can do further processing, but make sure that you perform valid operations based on the actual entity type. An entity record is defined as a union, so intellisense code completion in Visual Studio will happily allow you to access circle data members on a line, so it is up to you to make sure you do the correct things.

I’ll do two examples here to show some manipulation. The first example will find all circles in the drawing and change their color (Hint: The pentagram command we wrote earlier makes circles) to show how to manipulate entities in the drawing. The second will delete all map notes in the drawing and will thus showcase how to delete entries from the drawing list. If you look back at the pentagram example, you’ll also see that we used DLCopy there to make a copy of an entity already in the drawing list. This allowed us to just make a copy of the previous line and rotate it by the required 36 degrees.

There will be more operations you can do with the drawing list than we are talking about today, but these should be the operations you would normally need for working with a single drawing.

Note that hen you work with the drawing list, most commands want a handle to the drawing list. As long as you want to operate on the main drawing list in the drawing, you can just use NULL here.

Another important thing when you scan the drawing list is to specify the correct flags, depending on what you want, so let us look at the most common ones

  • DLS_Std: This is a standard drawing list scan. It will retrieve all entities in the main drawing list, but it will not process entities in sublists. Note that it will only return editable entities, that is entities not on a frozen or hidden layer. It will also skip deleted entities. It will treat sheets a bit special, i.e. it will completely ignore the existence of sheets, and return all entities from sheets as if they where in the main list, even if sheets are really sublists (this is an exception from the rule that it doesn’t process sublists)
  • DLS_RO: Allows the processing of read only entities. This is useful when you are scanning to get information from entities as opposed to manipulating thme (like the LIST command). With this flag set, entities from frozen layers are included.
  • DLS_All: Includes everything, even erased entities (erased entities are kept in the drawing list until the next save)
  • DLS_SL: Also includes entities found in sublists (Recursive scan)
  • DLS_SHEET: Processes sheets as their own entities instead of defaulting to scanning their contents. This overrides the behaviour from DLS_Std that entities from sheet appears as if they where in the main drawing list, with this flag specified, you would find the sheets themselves in the main drawing list, and have to access their sublists to get the entities on them.

These flags can be combined as usual, using either addition or logical or (DLS_Std+DLS_RO or DLS_Std|DLS_RO both work fine [For the mathematically inclined, this is just basic math, and as long as you work on binary flags, both operations will yield the same end result]).

Manipulating Entities

For this example, we’ll ask the user to provide a color, and then we will scan the drawing list for all circle entities, and change their color to the new one the user specified. Before running this command, use the the pentagram command we wrote earlier to draw a bunch of pentagrams, since they all contain circles. And just for the fun of it, we will increment the color each time so they don’t all end up with the same color.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void XPCALL XP2ColorCircle(void);
void XPCALL XP2ColorCircleColor(int Result,int Result2,int Result3);
pENTREC XPCALL XP2ColorCircleProcess(hDLIST hDList, const pENTREC pEntRec, const PARM parm1, PARM parm2);


char CList[] = "XP2LINE\0XP2PENT\0XP2NOTE\0XP2COLORCIRCLE\0\0";
PCMDPROC PList[] = { About, XP2Line, XP2Pent, XP2Note, XP2ColorCircle };


FORMST(lpszColor, "New Circle Color Base [Palette]:\0")
int color;

RDATA ColorReq =
{ sizeof(RDATA), RD_Color, NULL, RDF_C, (DWORD*)& color,
  (DWORD*)& lpszColor,RDC_ARROW, XP2ColorCircleColor, NULL, NULL, 0, NULL, 0 };


void XPCALL XP2ColorCircle() {
	
	ReqData(&ColorReq);
			   		 
}

void XPCALL XP2ColorCircleColor(int Result, int Result2, int Result3) {
	
	if(Result != X_OK) { CmdEnd();}

	DLScan(NULL, XP2ColorCircleProcess, DLS_Std, NULL, NULL);

	CmdEnd();

}

pENTREC XPCALL XP2ColorCircleProcess(void* hDList, const pENTREC pEntRec, const DWORD parm1, DWORD parm2) {
	
	UNREFERENCED_PARAMETER(parm1);
	UNREFERENCED_PARAMETER(parm2);
	UNREFERENCED_PARAMETER(hDList);

	const auto circle = XP_CIR2_CAST(pEntRec);

	if(circle) {
		
		circle ->CStuff.EColor2 = (color%=255)++;
		EDraw(pEntRec);

	}

	return 0;
}

 

Code Explanation

Line 1-3: Standard C++ function declarations.

Line 6-7: Adding the commands to CC3+.

Line 10: Prepare the color prompt. Note that when we prompt for a color, we can also right click to bring up the color picker, so we indicate this to the user by putting the word palette in square brackets. Square brackets are used to inform users about the default values/actions. The text here is only informative though, we need to actually implement default handling ourselves later.

Line 11: A variable to hold the color the user picks

Line 13-15: The RDATA for the request to come. Do note that the type of data we now request is RD_Color, this will automatically take care of the right click to bring up the palette, and it will also cause CC3+ to only accept valid color values (0-255) on the command prompt. Anything else entered will cause the command to fail (Result will be X_BAD)

Line 18-22: Main method for the XP2COLORCIRCLE command. All we do here is to request data using the RDATA we defined above.

Line 24-32: Method for handling the callback from the ReqData call.

Line 26: The usual check for valid user input

Line 28: Here we tell CC3+ to start scanning the drawing list. The list specified is NULL, which means the main drawing list, and the flag is set to DLS_Std which means it will scan the main drawing list only (including entities on sheets), but it will not include deleted entities, entities on hidden layers or entities on frozen layers. For each entity found in the drawing list, the XP2ColorCircleProcess function will be called.

Line 30: ending the command safely as usual. Note that all the calls to XP2ColorCircleProcess from line 28 will happen before we get to this point.

Line 34-50: Function for handling the individual entities from DLScan. Note that this function is called once for every entity, and it is up to us to check if the entity retrieved is one we want to process or not.

Line 36-38: We don’t care about some of the parameters to this functions, so by using these macros to tell the compiler about it, it knows it is not an oversight, and won’t complain during compilation.

Line 40: We use a macro from XT_Entities.h to cast the entity record to the appropriate type. The nice thing about this macro is that it will return a nullpointer if the entity is not the correct type, and the entity otherwise. This saves us from manually checking the Cstuff.Etype to figure out the correct type before casting. If we just perform a standard cast without checking, it will always be successful, but if we cast to the wrong entity type, we’re going to start corrupting things real fast if we modify things. Here, we try to cast it to a circle (CIR2) entity.

Line 42-47: Here we check on the entity type if it is valid. If it is a circle, the statements in the conditional will be executed, otherwise it wan’t the entity the we wanted, so we just let the function end without doing anything.

Line 44: Here we access the CStuff of the circle and update the color property (and increment the color variable at the same time so the next entity processed will have the next color in the palette [This will be bad if this value ever exceeds 255, so we also apply a little modulo division here])

Line 45: Redraw the entity on the screen to see the change.

Line 49: Standard behavior when scanning a drawing list is to return 0 as this will cause DLScan to just continue with the scan. Returning an entity record will stop the scan, and is appropriate if you have found what you where looking for and want the scan to end.

Deleting Entities

This example shows how to delete entities. It is similar to the previous one, just that we need to call DLDelete on the entities to remove them. Note that when you delete an entity, it isn’t deleted immediately, instead the entity record is just marked as deleted, and it will no longer show up on normal DLScan operations (although you can always specify DLS_All to include deleted entities in the scan). Deleted entities are purged from the drawing list on the next save, which is why undo doesn’t work after you have saved the drawing.

This example deletes all the map notes in the current drawing. It has no prompts or confirmations. Test it by making sure you have some map notes, then run the command, then check back to confirm all map notes are gone.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void XPCALL XP2DelNotes(void);
pENTREC XPCALL XP2DelNotesProcess(hDLIST hDList, const pENTREC pEntRec, const PARM parm1, PARM parm2);


char CList[] = "XP2LINE\0XP2PENT\0XP2NOTE\0XP2COLORCIRCLE\0XP2DELNOTES\0\0";
PCMDPROC PList[] = { About, XP2Line, XP2Pent, XP2Note, XP2ColorCircle, XP2DelNotes };


void XPCALL XP2DelNotes() {
	
	DLScan(NULL, XP2DelNotesProcess, DLS_Std, NULL, NULL);

	CmdEnd();

}

pENTREC XPCALL XP2DelNotesProcess(void* hDList, const pENTREC pEntRec, const DWORD parm1, DWORD parm2) {
	
	UNREFERENCED_PARAMETER(parm1);
	UNREFERENCED_PARAMETER(parm2);
	UNREFERENCED_PARAMETER(hDList);

	const auto note = XP_NOTE_CAST(pEntRec);

	if(note) {
		
		DLDelete(pEntRec);

	}

	return 0;
}

 

Code Explanation

Line 1-2: Standard C++ function declarations.

Line 5-6: Adding the commands to CC3+.

Line 9-15: Main method for the XP2DELNOTES command. This function starts the scan using standard flags.

Line 17-32: Function for handling the entity processing of the entities found by the scan.

Line 23: Attempt to cast the entity to a note type.

Line 25-29: Check if the entity is a note (the note variable isn’t a nullpointer), and if it is, call DLDelete on the entity record to mark it as deleted.

Sublists

There will always be sublists in your drawing list. Some of the main places you are going to find sublists are for sheet entities (all the entities on a given sheet is in a sublist on that sheet entity) and for symbol definitions (a symbol definition is treated like a single entity, but a symbol is in reality built up from multiple entities, and these are accessible via the sublist on the symbol definition). Symbol references with text attributes also contain sublists for storing these attributes. The following example first scans for sheets in the main drawing list, and then for each sheet, it scans the sublist from the sheet for symbols, and then shows a dialog for each sheet, listing the symbols (and their entity id’s) found on that sheet.

To test it, place a bunch of symbols in your drawing before running the command. Note that it will show a dialog for each sheet, even if there aren’t any symbols on it, I’ll leave it up to the reader to implement a check to change that behavior.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
void XPCALL XP2SheetList(void);
pENTREC XPCALL XP2SheetListProcess(hDLIST hDList, const pENTREC pEntRec, const PARM parm1, PARM parm2);
pENTREC XPCALL XP2SheetListSubProcess(hDLIST hDList, const pENTREC pEntRec, const DWORD parm1, DWORD parm2);


char CList[] = "XP2LINE\0XP2PENT\0XP2NOTE\0XP2COLORCIRCLE\0XP2DELNOTES\0XP2SHEETLIST\0\0";
PCMDPROC PList[] = { About, XP2Line, XP2Pent, XP2Note, XP2ColorCircle, XP2DelNotes, XP2SheetList };


char sheetName[64];
std::string sheetContent;

void XPCALL XP2SheetList() {
	
	DLScan(NULL, XP2SheetListProcess, DLS_Std|DLS_SHEET, NULL, NULL);

	CmdEnd();

}

pENTREC XPCALL XP2SheetListProcess(void* hDList, const pENTREC pEntRec, const DWORD parm1, DWORD parm2) {
	
	UNREFERENCED_PARAMETER(parm1);
	UNREFERENCED_PARAMETER(parm2);
	UNREFERENCED_PARAMETER(hDList);

	const auto sheet = XP_SHEET_CAST(pEntRec);

	if(sheet) {

		strcpy_s(sheetName,sheet->SName);
		sheetContent = "";
		
		hDLIST hSubL = DLGetSubList(pEntRec);
		if (hSubL != NULL) {
			DLScan(hSubL, XP2SheetListSubProcess, DLS_Std, sheet, NULL);
		}

		FORMSTPKT(sheetListOutput, "!01\r\n\r\n!02", 2)
			{ (DWORD *)sheetName, FT_Stg, FJ_Var, 5, FDP_User },
			{ (DWORD *)sheetContent.c_str(), FT_Stg, FJ_Var, 5, FDP_User },
		FORMSTEND

		FormSt(&sheetListOutput, RSC(FD_MsgBox));

	}

	return 0;
}

pENTREC XPCALL XP2SheetListSubProcess(hDLIST hDList, const pENTREC pEntRec, const DWORD parm1, DWORD parm2) {

	UNREFERENCED_PARAMETER(parm1);
	UNREFERENCED_PARAMETER(parm2);
	UNREFERENCED_PARAMETER(hDList);

	const auto symbol = XP_SYMREF_CAST(pEntRec);

	if(symbol) {
		sheetContent = sheetContent + symbol->SName + " (" + std::to_string(symbol->CStuff.Tag) + ")\r\n";
	}

	return 0;
}

 

Code Explanation

Line 1-3: Standard C++ function declarations.

Line 6-7: Adding the commands to CC3+.

Line 10-12: Variables for collecting data and show in the message box.

Line 13-19: Main method for the XP2SHEETLIST command. This method starts the scan as in earlier examples, but note the use of the DLS_SHEET flag which will return the sheets as entities instead of treating all their contents as part of the main drawing list.

Line 21-49: This method processes the entities return from scanning the main drawing list. Here, we wish to identify sheets, and get their name and sublist.

Line 27: Try to cast the entity record to a sheet entity.

Line 29-46: If the entity is a sheet entity, we process it.

Line 31: Copy the name of the sheet from the entity to the buffer variable.

Line 32: Clears the content from previous sheets.

Line 34: Retrieves the sublist from the sheet entity

Line 35-37: Check if the entity really had a sublist (it isn’t NULL), and if so, run a DLScan on the sublist (The XP2SheetListSubProcess will be called for each entity).

Line 39-44: Builds up a FormSt packet containing the sheet name and list of symbols on the sheet (gotten from XP2SheetListSubProcess below) and displays a message box with this information.

Line 51-64: This function processes the entities read from the individual sheet sublists.

Line 57: Try to cast the entity to a symbol

Line 59-61: If the entity is a symbol, grabs the name and id (tag) from the entity and concatinate it with existing data in the sheetContent string for use later (line 41).

Download

You can download my Visual Studio solution if you want to check it out. Note that for it to work right out of the box, you need to have the same setup as I have been describing in part 1 (The XP toolkit installed in C:\FCWXP and a copy of CC3+ for development purposes in C:\CC3Plus), otherwise you need to change the project properties.

Help and Discussions

The ProFantasy Community Forum has a dedicated category for Macros and XPs. I recommend you post any questions you may have over here. Comments on this blog entry is not a good place to get help, in part because I don’t get alerted when new comments are posted, and I don’t go around checking old posts for comments.

Comments are closed.