Developing a Vectorworks 2011 tool plug-in, TDD-style – Episode 3

With quite a sound class structure in place (and after returning from a refreshing summer vacation in Turkey), let’s see if we can put our new class strcuture to good use while trying to create the actual Vectorworks geometry (a line), test first – of course. Reading through the first series on TDD with Vectorworks, we used a model object for representing the geometric properties of the object and a rather thin layer to create the actual Vectorworks geometry from the model. How about trying this approach with a Vectorworks tool plug-in, too?

TEST_N(ToolPointsShouldBeReflectedAsStartEndPointOfLineObject)
{
	MockRedGreenLineTool vwTool;

	vwTool.AddPoint(WorldPt3(0, 0, 0));
	vwTool.AddPoint(WorldPt3(50, 0, 0));

	MockRedGreenLineObject* vwObject = vwTool.Create();
	
	CHECK_EQUAL(WorldPt3(0, 0, 0), vwObject->GetStartPoint());
	CHECK_EQUAL(WorldPt3(50, 0, 0), vwObject->GetEndPoint());
	
	delete vwObject;
}

We expect that the tool object will be able to create the line object for us and that the tool will take care of making sure that the start & end point of the line object will reflect the tool points.

In order to get the test to compile, we need to implement Create() for MockRedGreenLineTool.

class MockRedGreenLineTool : public RedGreenLineTool {

public:

	MockRedGreenLineObject* Create() const {
		MockRedGreenLineObject* lineObject = new MockRedGreenLineObject();

		lineObject->SetStartPoint(fToolPoints[0].fPoint);
		lineObject->SetEndPoint(fToolPoints[1].fPoint);

		return lineObject;
	}		

};

Now we need to come up with a simple implementation for MockRedGreenLineObject. Not a lot to see here – just a mock implementation.

class MockRedGreenLineObject {

public:
	
	void SetStartPoint(const WorldPt3& p) {
		fStartPoint = p;
	}
	
	void SetEndPoint(const WorldPt3& p) {
		fEndPoint = p;
	}
	
	virtual WorldPt3 GetStartPoint() const {
		return fStartPoint;
	}
	
	virtual WorldPt3 GetEndPoint() const {
		return fEndPoint;
	}
	
private:
	
	WorldPt3 fStartPoint;
	WorldPt3 fEndPoint;
};

Now the code compiles, links and the test passes. As in episode 1, the code is stuck in the “Mock” classes, but we need to drive it from Vectorworks. Let’s refactor us out of this mess.

First, we push the implementation of Create() up to the base class, RedGreenLineTool. However, RedGreenLineTool::Create() shouldn’t return an instance of MockRedGreenLineObject, but an instance of a more generic RedGreenLineObject class in order to accommodate creation of a Vectorworks-specific subclass. Creation of the particular instance is left to the subclasses of RedGreenLineTool.

We move the code from MockRedGreenLineObject to a new superclass RedGreenLineObject.

class RedGreenLineObject {

public:
	
	void SetStartPoint(const WorldPt3& p) {
		fStartPoint = p;
	}
	
	void SetEndPoint(const WorldPt3& p) {
		fEndPoint = p;
	}
	
	virtual WorldPt3 GetStartPoint() const {
		return fStartPoint;
	}
	
	virtual WorldPt3 GetEndPoint() const {
		return fEndPoint;
	}

private:
	
	WorldPt3 fStartPoint;
	WorldPt3 fEndPoint;
};

Code still compiles, links and passes the tests. Now for the surgery concerning RedGreenLineTool::Create():

class VectorworksRedGreenLineTool : public RedGreenLineTool {

	RedGreenLineObject* CreateRedGreenLineObject() const {
		return VectorworksRedGreenLineObject();
	}
};


class MockRedGreenLineTool : public RedGreenLineTool {

	RedGreenLineObject* CreateRedGreenLineObject() const {
		return MockRedGreenLineObject();
	}
};


class RedGreenLineTool {

public:

	RedGreenLineObject* CreateRedGreenLineObject() const = 0;

	RedGreenLineObject* Create() const {
		RedGreenLineObject* lineObject = CreateRedGreenLineObject();

		lineObject->SetStartPoint(fToolPoints[0].fPoint);
		lineObject->SetEndPoint(fToolPoints[1].fPoint);

		return lineObject;
	}		

};

Again, code compiles, links and our tests pass. What’s left? We need to hook up the classes to CTool_EventSink and create the Vectorworks geometry.

long CTool_EventSink::LegacyMain(long action, long message1, long message2)
{
	long result = 0;
	
	switch (action) {

		case kToolHandleComplete:
			fRedGreenLineTool->Create();
			break;
	}
};

Compiles, links. No surprises here. We start Vectorworks, select our tool, click twice to create a line – but nothing happens….yet. Ah, we forgot to create the geometry. How to do that? Here’s the simplest way of doing it which comes to my mind:

class RedGreenLineTool {

public:

	RedGreenLineObject* Create() const {
		RedGreenLineObject* lineObject = CreateRedGreenLineObject();

		lineObject->SetStartPoint(fToolPoints[0].fPoint);
		lineObject->SetEndPoint(fToolPoints[1].fPoint);

		lineObject->CreateGeometry();

		return lineObject;
	}		

};

Of course, the implementation of CreateGeometry() in MockRedGreenLineObject will be empty, whereas VectorworksRedGreenLineObject::CreateGeometry() is somewhat beefier:

class VectorworksRedGreenLineObject {

	virtual void CreateGeometry() {
		VWLine2DObj line(fStartPoint.x, fStartPoint.y, fEndPoint.x, fEndPoint.y);
		line.AddObjectToContainer(gSDK->GetDefaultContainer());
	}

};

We can easily deal with the non-testability of CreateGeometry(), as all the logic of the line object’s “model” itself is under test. And if something goes horribly wrong in CreateGeometry(), it should be easy to figure out.

RedGreenLineObject doesn’t feature an awful lot of sophisticated logic so far, but we are not done yet. The “spec” calls for creating a red or green line depending on where the user sets the clicks. Sounds like this attribute will find a good home in the RedGreenLineObject class. Speaking of it, determining the location of the clicks test first sounds like an interesting challenge. Let’s try to tackle that one in the next episode.

Previous Episode | Next Episode