Developing a Vectorworks 2011 Plug-in, TDD-style – Episode 6

We left off in Episode 5 trying to avoid Dr. Bob hitting us with a rolled-up newspaper as we forgot to account for the front tiles’s z position. Let’s see if we can fix this. First, we change the test for the front model origin to expect a WorldPt3 which, obviously, does contain a z-component.

TEST_N(FrontModelOrigin)
{
	SimpleCabinetModel simpleCabinet(600, 720, 800);
	
	FrontModel frontModel(simpleCabinet.GetFrontModelOrigin());
	
	CHECK_EQUAL(WorldPt3(SimpleCabinetModel::kBoardThickness, 0, SimpleCabinetModel::kBoardThickness), frontModel.GetOrigin());
}

Making this test pass requires a series of simple changes to FrontModel.

class FrontModel {

public:

	FrontModel(const WorldPt3& origin) :
		fOrigin(origin) {
	}

	WorldPt3 GetOrigin() const {
		return fOrigin;
	}

private:

	WorldPt3 fOrigin;
}

After making these changes gcc tells us that we should probably fix the tests FrontModelPositionOfFirstSegment and FrontModelPositionOfSecondSegment in order to make this thing compilable again. As in FrontModelOrigin, this requires us to replace WorldPt with WorldPt3 and we’re basically done.

With this out of the way, let’s concentrate on generalizing our FrontModel. So far, we’re able to calculate the position of the first and second segment / tile (assuming both fit into the cabinet). Looking at the screenshot in episode 0, it sure looks like the front tiles should be arranged in rows & columns. Let’s see if we can work towards this. How about computing the number of front tile rows & columns which “fit” into a given cabinet? Let’s start with a simple test – if the cabinet is too small, no front tiles will fit at all.

TEST_N(FrontModelNoRowsNoColumnsNotWideEnough)
{
	SimpleCabinetModel simpleCabinet(150, 720, 800);
	
	FrontModel frontModel(simpleCabinet.GetFrontModelOrigin(), 
						  simpleCabinet.GetFrontWidth(), 
						  simpleCabinet.GetFrontHeight());
	
	CHECK_EQUAL(0, frontModel.GetNumRows());
	CHECK_EQUAL(0, frontModel.GetNumColumns());
}

In order to be able to compute the number of rows & columns, we have to supply the FrontModel with the space available for placing front tiles. So, we need tests to for these functions. Let’s #if 0 the last test for a moment and write new tests for GetFrontWidth() and GetFrontHeight().

TEST_N(SimpleCabinetFrontWidthHeight)
{
	SimpleCabinetModel simpleCabinet(600, 720, 800);

	CHECK_EQUAL(600 - 2 * SimpleCabinetModel::kBoardThickness, 
				simpleCabinet.GetFrontWidth());
	CHECK_EQUAL(720 - 2 * SimpleCabinetModel::kBoardThickness, 
				simpleCabinet.GetFrontHeight());
}

Let’s make this test pass.


class SimpleCabinetModel {

public:
	
	WorldCoord GetFrontWidth() const {
		return fWidth - 2 * kBoardThickness;
	}
	
	WorldCoord GetFrontHeight() const {
		return fHeight - 2 * kBoardThickness;
	}
};

Backtracking to our test FrontModelNoRowsNoColumnsNotWideEnough, we change FrontModel‘s constructor (again).

class FrontModel {

public:
	
	FrontModel(const WorldPt3& origin, WorldCoord width, WorldCoord height) : 
		fOrigin(origin), fWidth(width), fHeight(height) {
	}
};

What was our intital goal? Ah, computing the number of rows & columns (in order to make the test pass). Well, that’s straightforward:

class FrontModel {

public:
	
	long GetNumRows() const {
		return 0;
	}
	
	long GetNumColumns() const {
		return 0;
	}
};

Indeed, it’s pretty straightforward for the test we’re trying to make pass. We just write enough code to make the test pass. And returning 0 makes the test pass. Let’s move on to the next test.

TEST_N(FrontModelThreeRowsTwoColumnsFits)
{
	SimpleCabinetModel simpleCabinet(2 * 200 + 2 * SimpleCabinetModel::kBoardThickness, 
									 3 * 150 + 2 * SimpleCabinetModel::kBoardThickness, 
									 800);
	
	FrontModel frontModel(simpleCabinet.GetFrontModelOrigin(), 
						  simpleCabinet.GetFrontWidth(), 
						  simpleCabinet.GetFrontHeight());
	
	CHECK_EQUAL(3, frontModel.GetNumRows());
	CHECK_EQUAL(2, frontModel.GetNumColumns());
}

Now is the right time to flesh out GetNumRows() and GetNumColumns().

class FrontModel {

public:
	
	long GetNumRows() const {
		return MyMathUtils::fdiv(fHeight, SimpleCabinetModel::kSegmentHeight);
	}
	
	long GetNumColumns() const {
		return MyMathUtils::fdiv(fWidth, SimpleCabinetModel::kSegmentWidth);
	}
};	

In order to get the number of front tiles fitting into a row, we need to divide the overall width available for the front tiles and divide it (as in division of integers) by the width of a segment. Same for the number of front tiles fitting into a column.

Where are we at? We now have code for calculating the number of tiles in x direction (columns) and z direction (rows). If we would have a function computing the x, y & z position of a front tile at a given row & column we could certainly beef up SimpleCabinetCreator::CreateFrom() with a nice loop placing all the front tiles. Let’s write a test for this function computing a tiles position:

TEST_N(FrontModelPositionOfARowColumn)
{
	SimpleCabinetModel simpleCabinet(1200, 720, 800);
	
	FrontModel frontModel(simpleCabinet.GetFrontModelOrigin(), 
						  simpleCabinet.GetFrontWidth(), 
						  simpleCabinet.GetFrontHeight());

	WorldPt3 expectedPosition = frontModel.GetOrigin() +
								WorldPt3(3 * SimpleCabinetModel::kSegmentWidth, 0, 
										 2 * SimpleCabinetModel::kSegmentHeight)
	CHECK_EQUAL(expectedPosition, 
				frontModel.GetSegmentPosition(3, 2));
}

Here’s the code to make the test pass:

class FrontModel {

public:
	
	WorldPt3 GetSegmentPosition(long column, long row) const
	{
		WorldPt3 position(GetOrigin());
		
		position += WorldPt3(column * fSegmentWidth, 
							 0,
							 row * fSegmentHeight);
		
		return position;
	}
};

And for the grand final of this episode, let’s tackle placing the front tile symbols in SimpleCabinetCreator::CreateFrom(). With the preparatory work we’ve done in this episode it should be pretty straightforward – and it is:

	
	void CreateFrom(const FrontModel& frontModel) {
		
		for (long i = 0; i  frontModel.GetNumColumns(); i++) 
			for (long j = 0; j < frontModel.GetNumRows(); j++) {
				VWObject frontSegment = gSDK->PlaceSymbolByNameN(frontModel.GetFrontSymbolName(), WorldPt(0, 0));
				frontSegment.MoveObject3D(frontModel.GetSegmentPosition(i, j)); 
				fSimpleCabinet.AddObject(frontSegment);
			}	
	}

Episode 6

Tune in next week when you hear Nurse Piggy say: “Are we done yet? I have a date with a certain frog for dinner… “.

Previous Episode | Next Episode