Thursday, November 25, 2021

Map Compasses (Part 5): Radial Lines and Circles

Welcome back to my blog series on implementing procedurally-generated map compasses!  In the previous post, I write the code for the language commands CIRCLE and SPACE.  In this posting, I'm going to work on commands for radial elements.

Radial elements are the parts of the compass that repeat at regular intervals in a circle around the center point of the compass.  For example, consider this very simple compass:

You could view this compass as having a line repeated every 90 degrees, plus a label “N" repeated every 360 degrees.  A more complex example:
Here's a compass with a lot of radial elements.  Labels of various sorts, but also short lines (and somewhat longer lines) that provide the degrees scale, some triangles to mark the diagonals on that scale, a set of smaller compass points and a set of larger compass points, and even some little dots for decoration in an inner ring.  (And everything else in the compass is some version of CIRCLE.)

To start with, I have to add some commands to the Compass Description Language (CDL) to describe radial elements.  There are a couple of ways I might do this.  I could make a separate command for every type of radial element, or I could do something like RADIAL ... END to wrap around commands that I want to place radially.  I'm going to use separate commands, partially because it makes the interpreter simpler and partially because I'd still need those commands to put inside the RADIAL loop anyway.

But I would like all the radial elements to have a similar syntax.  Looking at those radial elements in the complex example above, there are some parameters that seem to apply to all radial elements.  First, a starting point.  Some elements start at the top of the circle (like the larger compass points above) but some start elsewhere (like the smaller compass points above, which start at 45 degrees).  Second, the number of repeats.  The compass points above repeat 4 times, while the smaller lines in the degree scale repeat 360 times.  Third, there's a length or a size.  Some elements extend all the way to the center of the compass (like the compass points above) but some only extend partway to the center (like the lines that make up the degrees scale above).

Pulling all that together, I'll implement a simple radial line element:

RLINE(start, repeats, length, lineWidth, color)

This command will draw a radial line of the given length in the given color.  The length will start at the current radius and extend in towards the center of the compass.  But I'll also support a negative length, which will let me draw outward as well.  It will be handy to have a shorthand syntax that means “draw all the way to the center of the compass" so that I don't have to keep track of that distance when generating CDL.  Zero is conveniently unused in this scheme, so I'll use a zero length to mean “all the way to the center."

The first step in implementing this is adding it to the parser.   The rule for this command is long but straightforward:

# A radial line element
rlineElement -> "RLINE"i WS "(" WS decimal WS "," WS decimal WS "," WS decimal WS "," WS decimal WS "," WS dqstring WS ")"
{% data => ({op: "RLINE", start: data[4], repeats: data[8], length: data[12], width: data[16], color: data[20]}) %}
Other than the literal elements like the name and the parentheses, most of the command is decimals and double-quoted strings to define the parameters.  As with the other commands, it gets translated during post-processing into a Javascript object with the various parameters saved onto the object.

We also need to modify the list of legal CDL elements to include the RLINE element:
# A list of all the legal CDL elements
Element -> spaceElement | circleElement | rlineElement

A description that includes RLINE will now be parsed.  (Remember that when you modify the Nearley grammar file, you need to recompile it using the command line at the top of the file.)

The next step is to implement RLINE in the interpreter (in compass.js).  First, we need to add RLINE to our if-else in the interpreter that dispatches based on the op code:

	} else if (op.op == 'RLINE') {
// Draw radial lines Draw.rline(svg, center, radius, op.start, op.repeats, op.length, op.width, op.color); if (debug) console.log('Draw radial lines.'); } else {

I've filled in a call here to “Draw.rline" that will do the actual work of drawing the radial lines.  “Op" here is the Javascript structure built in the Nealey rule above, and op.start, etc., are the parameters.  Now I'll need to jump over to draw.js and write that function.

The heart of the function is a loop that draws the proper number of repeats around the circle.  

function rline(svg, center, radius, start, repeats, len, width, color) {
    for(let angle = start; angle < start+Math.PI*2; angle += (Math.PI*2)/repeats) {
    };
};

angle is where I am currently drawing.  It starts at the start angle and loops all the way around and back to the same spot.  I'm working in radians here, so a full circle is 2PI.  Every step advances 2PI/repeats, which means I'll draw repeat number of elements as I go around the circle.  From the angle and the radius I need to calculate the coordinates of the actual point for the element.  Recalling my old friend Sohcahtoa from high school geometry, and remembering to measure from the center, I add those calculations:

function rline(svg, center, radius, start, repeats, len, width, color) {
    for(let angle = start; angle < start+Math.PI*2; angle += (Math.PI*2)/repeats) {
	const sx = center[0] + radius * Math.cos(angle);
	const sy = center[1] + radius * Math.sin(angle);
    };
};

Actually I lied just a little bit right there.  In SVG (and in many computer graphics systems), the Y axis is reversed from your familiar days in high school geometry.  The origin of a window is typically at the upper left, and Y goes up as you move down from the origin.  In practical terms, this means I have to reverse the sign on the calculation of sy:

function rline(svg, center, radius, start, repeats, len, width, color) {
    for(let angle = start; angle < start+Math.PI*2; angle += (Math.PI*2)/repeats) {
	const sx = center[0] + radius * Math.cos(angle);
	const sy = center[1] - radius * Math.sin(angle);
    };
};

(And yes, I messed this up when I first wrote the code.  I always mess this up.)

With that corrected, I can do a similar calculation to determine the end point of the line, but instead of using radius, I'll subtract the length.  So a positive length will result in a point closer to the center, as desired.  And I need to remember the special treatment when length == 0, which indicates a line that goes to the center of the compass.

function rline(svg, center, radius, start, repeats, len, width, color) {
    for(let angle = start; angle < start+Math.PI*2; angle += (Math.PI*2)/repeats) {
	const sx = center[0] + radius * Math.cos(angle);
	const sy = center[1] - radius * Math.sin(angle);
	// Length == 0 indicates a line that goes to the center of the compass
	if (len == 0) len = radius;
	const ex = center[0] + (radius-len) * Math.cosine(angle);
	const ey = center[1] - (radius-len) * Math.sine(angle);
    };
};

Now that I have the start and the end of the line I can call a line drawing routine.  This is very straightforward:

function line(svg, start, end, width, color) {
	svg.append('line')
	    .style('stroke-width', width)
	    .style('stroke', color)
	    .attr('x1', start[0])
	    .attr('y1', start[1])
	    .attr('x2', end[0])
	    .attr('y2', end[1]);
};

This appends an SVG <line> element to the SVG.  Let me feed a CDL that uses a RLINE element into the test routine:

CIRCLE(2, “black", “none") RLINE(0, 8, 4, 2, “black")
And voila!  Eight tick marks around the inside of our circle.  Let's see if drawing to the center of the compass works:
CIRCLE(2, “black", “none") RLINE(0, 8, 0, 2, “black")
Whoo-hoo!  Pizza pie!

Before I get too happy, let's check a couple of other things.  How about a single line:
CIRCLE(2, “black", “none") RLINE(0, 1, 0, 2, “black")
Well, partial success.  It's a single line as expected, but pointing at 3 o'clock rather than 12 o'clock.  In retrospect, this makes sense.  The starting angle is 0, and a zero angle is right on the X axis.  I should have started at 90 degrees instead.  On the other hand, I'm always going to want to think of noon as the zero angle on a compass, so I'll shift the start angle by 90 degrees before I start drawing:
function rline(svg, center, radius, start, repeats, len, width, color) {
    for(let angle = start+(Math.PI/2); angle < start+Math.PI*2; angle += (Math.PI*2)/repeats) {
	const sx = center[0] + radius * Math.cos(angle);
	const sy = center[1] - radius * Math.sin(angle);
	// Length == 0 indicates a line that goes to the center of the compass
	if (len == 0) len = radius;
	const ex = center[0] + (radius-len) * Math.cos(angle);
	const ey = center[1] - (radius-len) * Math.sin(angle);
	line(svg, [sx, sy], [ex, ey], width, color);
    };
};

Now let me quickly implement another radial element, one that draws circles like this:

I won't go through adding this to the parser and the interpreter, as it is much the same as RLINE.  But here is the function that implements RCIRCLE:

function rcircle(svg, center, radius, start, repeats, len, width, color, fill) {
    for(let angle = start+(Math.PI/2); angle < start+Math.PI*2+(Math.PI/2); angle += (Math.PI*2)/repeats) {
	const sx = center[0] + radius * Math.cos(angle);
	const sy = center[1] - radius * Math.sin(angle);
	// Length == 0 indicates a line that goes to the center of the compass
	if (len == 0) len = radius;
	const ex = center[0] + (radius-len) * Math.cos(angle);
	const ey = center[1] - (radius-len) * Math.sin(angle);
	circle(svg, [sx, sy], len, width, color, fill);
    };
};

In this case “len" is the size of the circle to draw so I don't actually need the end point.  I left it in to illustrate how similar RLINE and RCIRCLE are -- they're identical except for the actual call to the drawing function.  All of the radial elements are going to share the same repeats loop and this suggests that can be abstracted out and not rewritten in every element.

To do that, I'll write a function that implements the repeats loop and takes the drawing function as a parameter.  That looks like this:

function repeat(svg, center, radius, start, repeats, op, fn) {
    let angle = start+(Math.PI/2);
    for(let i=0;i<repeats;i++) {
	fn(svg, center, radius, start, repeats, angle, i, op);
	angle += (Math.PI*2)/repeats;
    };
};

The new repeat function loops all the radial placements and calls fn with the SVG, the start angle, the number of repeats, the angle, the iteration and the original operation object.  Some of these parameters aren't needed for RLINE or RCIRCLE but (warning: foreshadowing!) they will be needed for future functions.  In particular, passing in the operation object will give our drawing routines access to the parameters that were in the original CDL command.

Now we can replace the call to RCIRCLE with a call to repeat.  This code in the interpreter:

	} else if (op.op == 'RCIRCLE') {
// Draw radial circles Draw.rcircle(svg, center, radius, op.start, op.repeats, op.length, op.width, op.color, op.fill); if (debug) console.log('Draw radial circles.'); } else {

becomes:

	} else if (op.op == 'RCIRCLE') {
// Draw radial circles Draw.repeat(svg, center, radius, op.start, op.repeats, op, Draw.rcircle); if (debug) console.log('Draw radial circles.'); } else {

Now I have to adjust the Draw.rcircle function slightly to handle the new parameters.

function rcircle(svg, center, radius, start, repeats, angle, i, op) {
    const sx = center[0] + radius * Math.cos(angle);
    const sy = center[1] - radius * Math.sin(angle);
    circle(svg, [sx, sy], op.length, op.width, op.color, op.fill);
};

And then a similar thing for RLINE:

	} else if (op.op == 'RLINE') {
// Draw radial lines Draw.repeat(svg, center, radius, op.start, op.repeats, op, Draw.rline2); if (debug) console.log('Draw radial lines.'); 
	}
I'll define the rline2 function to calculate the start and end points and call Draw.line:
function rline2(svg, center, radius, start, repeats, angle, i, op) {
    // Length == 0 indicates a line that goes to the center of the compass
    if (op.length == 0) op.length = radius;
    const sx = center[0] + radius * Math.cos(angle);
    const sy = center[1] - radius * Math.sin(angle);
    const ex = center[0] + (radius-op.length) * Math.cos(angle);
    const ey = center[1] - (radius-op.length) * Math.sin(angle);
    line(svg, [sx, sy], [ex, ey], op.width, op.color);
};

I've used the kind of awkward rline2 name here because I wanted to leave the original rline function in the code so that you could see it there.  

That's where I'll stop for today.  A reminder that if you want to follow along from home, you can download the Part 5 branch from the Procedural Map Compasses repository on Github.  Part 1 has more detailed instructions, but the short version (on Windows) is to double-click mongoose-free-6.9.exe to start up a web server in the Part 5 directory, and then select the test.html link.  Obviously you can browse the code and other files, and make changes of your own.  Try writing some CDL, inserting it into the test function, and using the button to generate it.  You can also try out this code on the web at https://dragonsabound-part5.netlify.app/test.html although you'll only be able to run the code, not modify it.

I've left the code set up to produce this:

But the CDL for all the examples is included in the code in comments just above the test function in compass.js.

Suggestions to Explore
  • Implement RCROSS that draws a radial cross element.  This is the same as RLINE but it adds a horizontal line across the existing line.  How did you deal with getting the horizontal line at the correct angle? Extend RCROSS to an asterisk by adding the two lines at 45 degree angles.

Wednesday, November 17, 2021

Map Compasses (Part 4): Implementing CIRCLE and SPACE

Welcome back to my blog series on implementing procedurally-generated map compasses!  In previous posts, I've started defining the Compass Description Language (CDL), built a parser for the language, and started creating an interpreter to draw the compasses.  At the end of the last posting I had prepared the web page for drawing.  The next step is to write the code to draw the current language commands (CIRCLE and SPACE).

Before I do that, let me discuss how drawing will work.  First of all, the interpreter will need to know where the compass is to be drawn (its center point) and how big the compass is (its total radius).  (For now I'll just set those parameters.)  Second, the compass will be (generally) drawn from the outside in, similar to how I handled drawing map borders.  The interpreter will have a “current radius" that will start at the total radius for the compass and be decremented after every drawing command.  For example, if the total radius of the compass was 100, the command CIRCLE(5, “black", “none") to draw a 5 pixel wide black circle would be drawn at 100 and decrement the marker from 100 to 95.  If that were followed by CIRCLE(1, “red", “none"), the red circle would be drawn at 95 and would just touch the inside edge of the black circle.

It's important to note that the marker indicates the outside edge of whatever is being drawn.  So to draw CIRCLE(5, “black", “none") at 100, I have to take into account the width of the line and draw so that the outside edge of the circle is at 100.

I'll begin by putting in the basic structure for drawing a circle:

function interpretCDL(svg, cdl, center=[0,0], debug=false) {
    let radius = 100;
    for(let i=0;i<cdl.length;i++) {
	const op = cdl[i];
	if (op.op == 'CIRCLE') {
	    // Draw a circle
	    Draw.circle(svg, center, radius-op.lineWidth/2, op.lineWidth, op.lineColor, op.fillColor);
	    radius -= op.lineWidth;
	    if (debug) console.log('Draw a circle.');
	} else if (op.op == 'SPACE') {
	    // Skip some space
	    if (debug) console.log('Skip some space.');
	} else {
	    console.log('executeCDL encountered an unknown opcode: '+op.op);
	};
    };
};
I've done a couple of things here.  First, I'm now passing the SVG element and the center of the compass into the interpreter so it will know where to draw.  Second, I've added the radius and started it at 100.  Where the interpreter handles the CIRCLE operation, I've added in a call to Draw.circle, which will be responsible for actually drawing the circle.  Note the third argument to Draw.circle, which is the radius of the circle.  Naively this is the current radius, but I have to back off by half of the line's width as well, or else the outside of the circle will go outside of radius.  After the circle is drawn I move the radius in by the width of the circle line, so that the next operation will start in the right place.

Over in draw.js is the code for Draw.circle:
function circle(svg, center, radius, lineWidth, lineColor, fillColor) {
    svg.append('circle')
	.attr('cx', center[0])
	.attr('cy', center[1])
	.attr('r', radius)
	.style('stroke-width', lineWidth)
	.style('stroke', lineColor)
	.style('fill', fillColor);
};
This is straightforward; it simply adds the SVG command to draw a circle to the SVG element that was passed into the interpreter.  If you're working in something other than SVG (say, canvas) then you can replace this with the appropriate circle drawing code for your display.

Testing this with “CIRCLE(1.5, “black", “none")" produces this:
Here's a more interesting example CIRCLE(2, “black", “none") CIRCLE(2, “red", “red") CIRCLE(2, “black", “none"): 
I've set the fill color of the second circle to red, so the inside of that circle is entirely red.  The next circle goes down on top of that with no fill color.  But no fill color doesn't hide the red, it just doesn't cover it over.  So how do you get a band of color instead of a whole circle of color?  You use a very wide line width, like so CIRCLE(2, “black", “none") CIRCLE(20, “red", “none") CIRCLE(2, “black", “none"):
What about the other command, SPACE?  This adds space between elements of the compass.  But the spacing of the elements of the compass is controlled by the current radius.  So to add space between elements, we just need to subtract that space from the radius:
	} else if (op.op == 'SPACE') {
	    // Skip some space
	    radius -= op.n;
	    if (debug) console.log('Skip some space.');
	}
So now we can do this: CIRCLE(2, “black", “none") SPACE(2) CIRCLE(2, “black", “none") SPACE(2)  CIRCLE(2, “black", “none"):
Interestingly, we can use negative numbers in the SPACE command to move “backwards" -- making the radius larger.  So, for example, we can draw a large band of color, go backwards to draw a line in the middle of the band, and then go forward to where we started like this CIRCLE(2, “black", “none") CIRCLE(20, “red", “none") SPACE(-11) CIRCLE(2, “green", “none") SPACE(9) CIRCLE(2, “black", “none"):
I'll leave it to the reader to puzzle out the CDL, but this ability to move in and out will be handy when we're doing the procedural generation of compasses.

If you want to follow along from home, you can download the Part 4 branch from the Procedural Map Compasses repository on Github, get the test web page up and open the console.  Part 1 has detailed  instructions, but the short version (on Windows) is to double-click mongoose-free-6.9.exe to start up a web server in the Part 4 directory, and then select the test.html link.  Obviously you can browse the code and other files, and make changes of your own.  Try writing some CDL, inserting it into the test function, and using the button to generate it.  You can also try out this code on the web at https://dragonsabound-part4.netlify.app/test.html although you'll only be able to run the code, not modify it.

Next time I'll start on generating radial elements.

Suggestions to Explore
  • The CIRCLE command draws a solid circle.  It might be useful to draw a dashed circle as well.  There are various ways to do this, but the easiest is to use the “stroke-dasharray" style in SVG.  Modify the CIRCLE command to take another parameter with the value for stroke-dasharray, and then use stroke-dasharray in Draw.circle to implement dashed circles.  How did you handle solid circles?

  • If you implemented the “no fill" version of the CIRCLE command in Part 2 in the parser, you can now implement that command in the interpreter.  Did you change interpretCDL() or Draw.circle, or both?  Why did you choose that approach?

  • My Map Borders Description Language also used the idea of a cursor and drawing from the outside inward, and also had a command like SPACE for moving the cursor.  MBDL also implemented a command that would save the current value of the cursor and then return to it later.  Implement that idea in CDL -- a command to REMEMBER() the current cursor value and a command to RECALL() the cursor value.  What happens if you REMEMBER() twice before RECALL()?  What happens when you RECALL() twice?  How would you extend this idea so that you could REMEMBER and RECALL multiple different values?

Wednesday, November 10, 2021

Map Compasses (Part 3): A Minimal CDL Interpreter

Welcome back to my blog series on implementing procedurally-generated map compasses!  You may recall that by the end of the previous blog post I had a parser in Nearley that could recognize compass descriptions consisting of two commands:  CIRCLE and SPACE.   Given the description 'SPACE(10) CIRCLE(1.5, "black", "none")' our parser confirms this is legal and returns a parse tree with two items at the top-level, corresponding to the SPACE and the CIRCLE commands.  That's nice, but how does it get us closer to drawing a circle?

The commands I've created actually look something like Javascript function calls.  If I had a Javascript function called “CIRCLE" that drew a circle I could just about execute the CIRCLE command as soon as it was parsed and recognized.  And in fact Nearley provides you the capability to do that.  To any Nearley rule, you can attach some Javscript to be executed when the rule is (successfully) completed.  So I can take the CIRCLE rule:

circleElement -> “CIRCLE" “(" decimal “," dqstring “," dqstring “)"

and augment it with some Javascript that should be run when the rule is complete: 

circleElement -> “CIRCLE" “(" decimal “," dqstring “," dqstring “)" {% CIRCLE(data) %}

The Javascript to be run is enclosed in “{%" and “%}" and must be a function that takes a single argument.  In this case, I'm calling a Javascript function called CIRCLE and passing in “data" as an argument.  What's this “data" thing?  It's the parse tree result for this rule.  In this case, it's an array with 8 items.  The first item is the string “CIRCLE".  The second item is the string “(".  The third item is the string that matched the “decimal" rule.  And so on.  So the CIRCLE Javascript function could use that data array to decide how to draw the circle and draw it right as the description of the circle is parsed.  If I extended that to all parts of our language, I could draw the complete compass as the CDL description was being parsed in Nearley.

That's pretty cool, but there are a couple of reasons I don't want to draw that way.  First of all, the kind of Javascript allowed in the post-processing step of Nearley rules is pretty limited.  For example, I probably want to draw that circle in a particular page element but there isn't any easy way to pass in that information.  Second, I would be limited to drawing routines that are neatly encompassed in a single Nearley rule.  Doing something like saving a color and reusing it later would be difficult.

A more flexible approach is to use the post-processing to translate into an “intermediate" language -- some kind of data structure that's easier to use than text -- and then have an interpreter that translates from the intermediate language into a drawing.  For the intermediate language I'm going to use a Javascript object for each CDL command and I'll fill the object in with an operation type and the parsed arguments.

 circleElement -> “CIRCLE" “(" decimal “," dqstring “," dqstring “)"
{% data => ({op: “CIRCLE", lineWidth: data[2], lineColor: data[4], fillColor: data[6]}) %}

So now I have a Javascript arrow function that takes the parse result from this rule and uses it to fill in a Javascript object.  Nearley will now return this object instead of the parse result.  Once I've done this for all the commands in the Compass Description Language the parser will return a list of parsed objects instead of the parse tree.

The rule for the space command is straightforward:

spaceElement -> “SPACE" “(" decimal “)" {% data => ({op:SPACE, n: data[2]}) %}

There's one more change that I need to make in the top-level rule, mostly for convenience:

compassBG -> BGelement:+ {% data => data[0].map(a => a[0]) %}

Without this fix, every object is returned as an array of a single element.  (In general, there might be multiple return values for every rule, but this grammar always has only one.)  This strips off all those unnecessary arrays. 

With these changes in place, when I run the parser the result is in the intermediate language -- an array of objects:

Okay, we have a program!  Now we need an interpreter.

An interpreter is a computer program that takes a computer program in a (usually) different language and interprets it, i.e., makes the program do what it is supposed to do.  Of course writing an interpreter for a language like Javascript is a difficult undertaking, but CDL is a very simple language and I'll be able to write a very simple interpreter.

A program in CDL is just a list of objects, so at a high level the interpreter will just be a loop that runs through the list of objects and interprets each object based upon the operation:

function interpretCDL(cdl, debug=false) {
    for(let i=0;i<cdl.length;i++) {
	const op = cdl[i].op;
	if (op == 'CIRCLE') {
	    // Draw a circle
	    if (debug) console.log('Draw a circle.');
	} else if (op == 'SPACE') {
	    // Skip some space
	    if (debug) console.log('Skip some space.');
	} else {
	    console.log('executeCDL encountered an unknown opcode: '+op);
	};
    };
};

This doesn't do much yet, but it's enough code to test out.  I'll modify the code on the Test button so that it passes the parsed CDL to the interpreter.

function test() {
    console.log('About to test.');
    const result = interpretCDL(parseCDL('SPACE(10) CIRCLE(1.5, "black", "none")', true), true);
};

And here's the result:

So we've taken a simple program in CDL, parsed it and interpreted it!

That's all fine, but eventually we're going to want to do more than print log messages to the console, so let's work on setting up an area to draw to the screen.  There are basically two steps to this:  (1) establish an element on the web page to hold the drawing, and (2) get a pointer for this element to the Javascript program.

To draw in SVG, you need to have an <SVG> element in the web page.  I've already done this in the test.html page in the repository:
      <div id="map" style="margin-left:10px;">
	<svg id="svg" width="500" height="500"></svg>
      </div>

The basic structure of test.html is stolen from Dragons Abound.  There's a column along the left side of the web page that is reserved for controls like the Test button, and the remainder of the page is the “map" area and is filled with a 500x500 SVG.

To pass the SVG element into the Javascript requires some more work in the HTML file for the web page.  That part looks like this:

    <script type="module">
      import Compass from './compass.js';
      var testButton = document.getElementById("testButton");
      var svg = d3.select("#svg");
      testButton.onclick = function () {
	  Compass.test(svg); };
    </script>
I've used the HTML <script> tag to add Javascript to the web page.  After importing the compass code, I use getElementById() to get a pointer to the Test button.  getElementById() is a function that walks through HTML to find an HTML element with the specified name.  In this case, the “document" variable refers to the current web page, so document.getElementById() searches the current web page to find a named element.   Once I've found the Test button I can attach the test function from my compass code to the button so that it runs when the button is clicked.

For the SVG element I do something a little bit different.  Here I use the D3 select function to find the SVG element.  This returns a D3 selection instead of the actual SVG element.  D3 is a fantastic Javascript library that makes working with SVG much simpler than trying to manipulate the SVG directly.  D3 is really intended for making data visualizations, not fantasy maps.  I'm using it largely because I started years ago with Martin O'Leary's mapmaking code, and he used D3 so I did too.  But I've found it useful enough to stick with it.

Lastly, let's check to make sure the SVG is working (and demonstrate how D3 eases using SVG).  I'll add some code to draw a purple circle to the test function.  That looks like this:
    svg.append('circle')
	.attr('cx', 25)
	.attr('cy', 25)
	.attr('r', 25)
	.style('fill', 'purple');
D3 adds methods to SVG selections, so adding new elements to the SVG is a simple as appending the proper object type and setting attributes and styles as necessary.
And that's all it takes.

A reminder that if you want to follow along from home, you need to download the Part 2 branch from the Procedural Map Compasses repository on Github and get the test web page up and open the console.  Part 1 of this series has more detailed instructions, but the short version (on Windows) is to double-click mongoose-free-6.9.exe to start up a web server in the Part 3 directory, and then select the test.html link.  You can also try out this code on the web at https://dragonsabound-part3.netlify.app/test.html although you'll only be able to run the code, not modify it.

Next time I'll start implementing the interpreter routines for the CDL commands.

Suggestions to Explore

  • The printed output of the CDL parser isn't very useful because the returned values just print as “[object Object]".  Write a function to “pretty print" the CDL objects in some format that shows the operation name, e.g., “{CIRCLE op}" and use this to improve the debug output.

  • Read about D3 and see if you can add lines to the purple circle from the center out to 12 o'clock, 3 o'clock, 6 o'clock and 9 o'clock.

  • The if-then-else at the heart of the interpreter exists to associate the operation name (e.g., SPACE, CIRCLE) with the code to implement that operation.  Another way to go from a string to an associated value in Javascript is to build an object that you use as a code dictionary:
    let code = {'CIRCLE': function () { console.log('Draw a circle.'); },
                'SPACE': function () { console.log('Skip some space'); } };
    
    Modify interpretCDL() to use a code dictionary as above.  What are the advantages / disadvantages of this approach compared to the if-then-else approach?