0086: Nodes-n-noodles V – The Drawing Routines

Before digging into the details of drawing the node, let’s do a quick summary of what we’re up to with the MoveableNode. Here’s a visual of what we’re aiming for (in case you weren’t here last time):

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

Note: You’ll notice that the above link doesn’t lead to the same source file we used last time. That code used an external image whereas this one has the drawing routines mentioned above.

So, here’s a summary of what we’re doing…

  • The MoveableNode is based on a the DrawingArea widget.
    • It’s the parent object, and
    • it has no drawing routines of its own.
    • This parent object’s properties determine the overall size of the MoveableNode.
  • The Shapes—the circles and rounded rectangles that make up the MoveableNode—are child objects.
    • Each shape has its own drawing routine, and
    • does its own preparatory calculations for shape, size, and color.
    • Each shape also keeps track of its location within the DrawingArea that is the MoveableNode.
    • Shapes are drawn in z-depth order so, for instance, the circles used as terminals aren’t masked by the larger shape of the MoveableNode itself.

And the reason it’s done like this is so that when we get around to moving this MoveableNode object, all its decorations and details come along for the ride without any extra work on our part.

Okay, that’s out of the way. Let’s get at it…

The MoveableNode Class

Compared to our first MoveableNode with its external image, this one is a bit more crowded. An Image is far less code, but it’s also external and, therefore, less portable, so in moving toward a more self-contained approach, here’s where we stand:

class MoveableNode : DrawingArea
	NodeShape nodeShape; // appearance of the node
	NodeHandle nodeHandle; // appearance of the drag handle
	NodeTerminalIn nodeTerminalIn; // appearance of the input terminal
	TerminalInStatus terminalInStatus; // appearance of the input terminal's status block
	NodeTerminalOut nodeTerminalOut; // appearance of the output terminal
	TerminalOutStatus terminalOutStatus; // appearance of the output terminal's status block
	int width = 113, height = 102;

Here in the preamble, we declare all those shapes I mentioned. What we’re going to end up with is in the screen shot above. Please note that there are six sub-shapes that make up the MoveableNode and each is an object.

		nodeShape = new NodeShape();
		nodeTerminalIn = new NodeTerminalIn();
		terminalInStatus = new TerminalInStatus();
		nodeTerminalOut = new NodeTerminalOut();
		terminalOutStatus = new TerminalOutStatus();
		nodeHandle = new NodeHandle();
	} // this()

In the constructor, each of the sub-shapes is instantiated and the onDraw() callback is hooked up.

	bool onDraw(Scoped!Context context, Widget w)
		setSizeRequest(width, height); // without this, nothing shows

		// call sub-objects' draw routines?
	} // onDraw()

} // class MoveableNode

And, naturally, in the onDraw() callback, the drawing routines are called. As mentioned, these routines are encapsulated in their respective classes/objects, so at this point, we don’t need to concern ourselves with them any more than we have.

And I’d like to point out that the terminalStatus shapes are based on the nodeHandle shape which is derived from the nodeShape, so a lot of this code is being reused… sort of. In the interests of full-blown inheritance, I could have created one parent class for a general rectangular shape with rounded corners and then derived all the rest from it. But for clarity, there is some repetition. I mean, it’s not like we have to be careful how much RAM we use, right? So, let’s just set our metaphorical elbows akimbo and get on with life.

I did go so far as to outline two classes for a generic NodeTerminal shape and a generic TerminalStatus shape and from there, derived the -In and -Out versions of both. But that’s as far as I went with it. If you’re up for a bit of fun, you might take it upon yourself to design a master shape class and derive all the rounded rectangle shapes from it.

The Sub-shapes

Each of the sub-shapes keeps track of:

  • rim and fill colors, and
  • it’s offset from the MoveableNode origin.

Drawing routines are in the sub-shape… unless the sub-shape is derived from a parent shape. In those cases (specifically, the NodeTerminalIn/Out and TerminalStatusIn/Out) drawing routines are in the parent classes NodeTerminal and TerminalStatus.

Have a gander at the sub-shape classes to see what I mean. Here’s the NodeTerminalIn class and its parent/root class, the NodeTerminal:

class NodeTerminalIn : NodeTerminal
	double[] _rimRGBA = [0.129, 0.243, 0.608, 1.0];
	double[] _fillRGBA = [1.0, 0.706, 0.004, 1.0];
	int _xOffset = 6, _yOffset = 34;
		super(_xOffset, _yOffset, _fillRGBA, _rimRGBA);
	} // this()
} // class NodeTerminalIn

class NodeTerminal
	double _radius = 5,
			 _lineWidth = 2;
	int _xOffset, _yOffset;
	double[] _fillRGBA, _rimRGBA;
	this(int xOffset, int yOffset, double[] fillRGBA, double[] rimRGBA)
		_xOffset = xOffset;
		_yOffset = yOffset;
		_fillRGBA = fillRGBA;
		_rimRGBA = rimRGBA;
	} // this()
	void draw(Context context)
		// set up the path and color
		context.arc(_xOffset, _yOffset, _radius, 0, PI * 2);
		// fill the circle
		context.setSourceRgba(_fillRGBA[0], _fillRGBA[1], _fillRGBA[2], _fillRGBA[3]);
		// color the path
		context.setSourceRgba(_rimRGBA[0], _rimRGBA[1], _rimRGBA[2], _rimRGBA[3]);
	} // draw()
} // class NodeTerminal

By and large, each class breaks down the same way. The preamble has:

  • the origin of the shape as an offset from the origin of the parent class/object, MoveableNode,
  • the sub-shape’s dimensions along with the radius, and
  • colors, line width, etc.

Because a circle (i.e. a NodeTerminal) only needs a point of origin and a radius in order to draw itself, there is one set of variables that aren’t used here. They only show up in the rectangular shapes (NodeShape, NodeHandle, and TerminalStatus):

double[] northEastArc, southEastArc, southWestArc, northWestArc;

These four 2-element arrays keep track of where to begin and end drawing each of the rectangle’s rounded corners.

The constructors for the sub-shapes are all about doing prep work for the drawing routines and the draw() function carries out the actual drawing. And I should point out that if these classes/objects were standalone, each draw() function would become an onDraw() callback hooked up to the onDraw signal for each object. But because of how we’re doing this—calling all the draw() functions from the MoveableNode.onDraw() parent callback—we don’t need to hook up the signals for these shapes. In fact, there are no signals at this level to hook them up to, anyway.

Everything else is very much like what we covered in earlier posts for Cairo drawing. The only difference, really, is that we pass the MoveableNode’s drawing Context to each sub-shape’s draw() function.


Full disclosure: I didn’t start with the image of the node and develop the drawing routines from it. I started with a hand-drawn node, went to an Inkscape drawing, then developed the drawing routines. Once I got the kinks ironed out and the code was working, I did a screen shot and created the image file from there. So, in fact, I put the cart before the horse. (Don’t tell anyone.)

Next time, we’ll get into hotspots. See you then.

Comments? Questions? Observations?

Did we miss a tidbit of information that would make this post even more informative? Let's talk about it in the comments.

You can also subscribe via RSS so you won't miss anything. Thank you very much for dropping by.

© Copyright 2023 Ron Tarrant