Friday, April 29, 2022

Map Compasses (Part 17): Compass Points and More

Welcome back to my blog series on implementing procedurally-generated map compasses!  For the past few posts I've worked on elaborating the “rings" that appear behind the compass points in compasses like these:

Now I want to spend a little time on the compass points.

There isn't nearly as much variation in compass points as in rings.  One common variation is to turn the compass points into some sort of arrowhead or other type of artistic pointer, as in this example:

I'm not going to try to replicate something like this.  It's certainly possible to draw arrowhead like compass points, but I'm not sure I can do that with much variety and more importantly, I don't particularly like that style.  If you love it, please have a go and let me know what you come up with.

Sticking to the traditional style of compass point, the main variations are straight or wavy, and the number of points used.  I covered how to draw wavy compass points back in Part 10, so I won't go over that again.  As far as the number of points go, so far the procedural generation rules always generate eight points.  So let's see about varying that.

To start with, I can modify the rule to create compasses which only have points for the four cardinal directions:
<topLayer> =>  <cardinalPoints> | <interCardinalPoints> <cardinalPoints>;

That works fine, and looking at a dozen examples, they all look acceptable, if a little plain in most cases.  But that's not necessarily a bad thing.

In the other direction, I can add the interordinal points.  These are eight points that fall between the intercardinal (also called ordinal) points.
<topLayer> =>  <cardinalPoints> |
<interCardinalPoints> <cardinalPoints> |
<interOrdinalPoints> <interCardinalPoints> <cardinalPoints> [100];
<interOrdinalPoints> => RECALL("insideEdge")
SPACE(Utils.randIntRange(7,12))
<interOrdinalPoint>;
<interOrdinalPoint> => RPOINT(0.39269908, 8, Utils.randRange(0.80,0.925),
1, 1, "black", "white", "black") |
RWAVE(0.39269908, 8, Utils.randRange(0.80,0.925),
1, 1, "black", "white", "black");

The interordinal points are also measured from the inside edge of the first ring, but they're 7-12 pixels closer to the center so that they'll never be longer than the intercardinal points.   The interordinal points start at PI/8 (i.e., 0.39269908), which places the first one between N and NE.

Because there are 8 of these points instead of 4, if I use the span value as for the other points, these points will be half as wide.  That's fine; the finer divisions are usually drawn with finer points.

One question is whether the interordinal points should be wavy when the intercardinal points are wavy.  To my eye, it seems fine to have straight points mixed with wavy points:
One problem I'm noticing as I generate examples are compasses like this:
where the points are overlapping the inside ring in confusing ways.  It's worse when the inner ring is a thick black band:
Looking through the example compasses and giving this some thought, I think the problem is mostly that a compass point shouldn't end inside a ring; it should either end before the ring or cross completely over the ring.  If I temporarily change the rules so that all of the pointers are as long as the cardinal pointers, that seems to bear out my hypothesis:
That's not the prettiest of compasses, but at least the compass points are distinct.  However, making the intercardinal and interordinal points cross completely over the inner ring (but not the outer ring) doesn't work as well:

That's at least partly because there isn't enough visual separation between the two rings to make the points stand out.  I can adjust the ring spacing to help with this.
That works much better, I think.  But problems still remain when the inner ring has radial elements:
Since the problem arises from the overlapping of the points and the radial elements, one solution might be to shift the elements so that they are between the points:
That seems like it will work, but there are some complications.  This works for a number of radial elements that is a multiple of the number of gaps between the compass points.  When there are two levels of compass points, the number of gaps is 8, so 8, 16, 24, 32, etc. radial elements will work.  But when there are three levels of compass points, the number of gaps is 16, so this will work with 16, 32, 48, etc.  But it won't work with 8 or 24 because the elements will fall in strange places:
There's no offset that results in a symmetrical arrangement.  So this will require replicating all the radial element rules once for each case.  Worse, this approach doesn't make sense for an outer ring of radial elements.  The whole point of this is to move the radial elements off of the compass points, but in the outer ring that's desirable:
so there will need to be another set of rules for the outer ring.  Rules tend to proliferate in these sorts of systems, but this seems a bit excessive.

What I need is way to remember a decision that was made in one rule (e.g., to show intercardinal compass points) and then use that decision in a later rule (e.g., to select the right number and offset for radial elements).  Right now Lodestone doesn't have that capability, so I'll have to add it.

The way I'll do this is to combine the embedded Javascript capability and the one-time rules along with some modifications to the rules engine.  In the end, this will let me write rules like this:
<rule1> => <first> <@tmp>=6 <second>;
[...]
<rule3> => "The value is: " <$tmp>;The @ syntax lets me assign a value to a one-time rule within the embedded Javascript, and then I can use the new value later just as I would any one-time rule.To start, I'll modify the Loadstone grammar to accept these kind of references. That starts with adding a new class to the lexer: // // One-time non-terminal reference in the form <@test> // otntermref: /<\@[^>]+>/, This looks exactly like a one-time non-terminal, except it expects an @ instead of a$.  The second part is to allow these references to show up on the right-hand side of rules, which I can accomplish by adding them to the list of legal elements:#
# An element can be a non-terminal (e.g., <test>), a one-time
# non-terminal (e.g., <$test>), a reference to a one-time # non-terminal (e.g., <@test>), a string of characters (e.g., SPACE), # a quoted string of characters (e.g., 'this here'), embedded # Javascript (e.g., Math.PI/2), a weight (e.g., [50]), or # the character for an alternative (e.g., |) # element -> %nterm | %otnterm | %otntermref | %string | %qstring | %jscript | %weight | %or In truth, I only want them to appear inside embedded Javascript, and this will allow them to appear anywhere on the right-hand side, but I'll live with that for the moment.Now I need to modify the Lodestone rules engine so that it knows how to deal with these references. That's done by adding this code to executeRules: // If it's a reference to a one-time terminal, then replace the // reference with the Javascript to reference where the value // of the one-time terminal is kept. if (current.type == "otntermref") { output = output.concat('otValues["'+current.value.replace('@','$')+'"]');
continue;
};What this does is replace a reference like “<@test>" with “otValues[''<$test>'']". The latter is the Javascript code for where the values of one-time non-terminals are stored. So now if I write embedded Javascript like this: <@test> = 15 it will be translated into otValues["<$test>"] = 15before it is executed as Javascript.  So 15 will get stored into <$test> -- or at least it will if otValues evaluates to the proper object when the Javascript is executed.To ensure this is a little tricky. I want embedded Javascript to execute as if it were executing in the context (e.g., closure) of the function from where executeRules is called. This allows the user to do something like this to define a value: let defaultWidth = 1; And then reference that in the embedded Javacript in a rule. But otValues isn't in this context, so as it stands this reference will fail. To address this, I have to add otValues to the context for the embedded Javascript. That context is defined by the function the user provides, e.g., const cdl = executeRules('<compass>', rules, s => eval(s)); In this case, the last argument that defines a function that calls eval. To add otValues to this context, I'll add another parameter to the context function which (critically!) I will also call otValues: const cdl = executeRules('<compass>', rules, (s, otValues) => eval(s)); (In fact, I could have called this parameter anything, as long as I used the same name in the @ translation above. But it's convenient to use the same name consistently.) The last step is to provide the real otValues as the value of that parameter. This happens when this function gets called in executeRules: // Execute in the provided context. const execVal = String(context(expanded, otValues)); Now the real otValues gets matched up with the parameter otValues and is available to eval when a one-time non-terminal references is used! (Whew! I know that's a little difficult to follow, but it's a complex idea and I've done the best I can.)One trickiness remains. When you use embedded Javascript, the return value gets substituted back into the rule where the embedded Javascript was found. In a case like this where we're using the Javascript for a side effect such as setting a value:<rule1> => <first> <@tmp>=6 <second>;we probably won't want the value of the Javascript (6 in this case) to be inserted into the rule. To avoid this, I need to modify the Javascript to return an empty string by adding a second statement that will evaluate to the empty string, i.e., <rule1> => <first> @tmp=6; "" <second>; [...] <rule3> => "The value is: "$tmp;(You can also use the Javascript comma operator here.)  This is a little clumsy, but certainly workable.Now let's put this to work for our radial elements.  Recall from above that different numbers of radial elements work for the inside rings depending upon whether or not the compass has two levels of pointers or three levels.  The number of radial elements is controlled by $numRadialElements, so I can set then when the choice for levels of compass points is made:<topLayer> => <cardinalPoints> @numRadialElements=Utils.randElement([4,8,12,16,20,24,28,32]),"" | <interCardinalPoints> <cardinalPoints> @numRadialElements=Utils.randElement([8,16,24,32]),"" | <interOrdinalPoints> <interCardinalPoints> <cardinalPoints> @numRadialElements=Utils.randElement([16,32]),"";Remember that inside the backticks is Javascript, so I'm using a utility function that picks a random element of an array to make the choice of how many radial elements to use.Sadly, this isn't quite right. The problem is that the bottom layer gets created first: <twoLayerCompass> => <labels> REMEMBER("start") <bottomLayer> <topLayer>;which means the radial elements have already been created before the <topLayer> rule decides how many points there will be. I need to modify the rules to choose the level of compass points before the bottom layer is drawn. That's easy enough to do with a one-time rule:$pointLevels => 1 | 2 | 3;But how will I use that to control the <topLayer> rule that actually draws the compass points?  The trick is to use the choice weightings.Recall that the choice weightings are numbers in square brackets that control how often a choice is selected in a rule.  For example this rule:<ringOrScale> => <ring> [5] | <scaleRing>;uses choice weightings so that rings are selected 5x as frequently as scales.  Two features of choice weighting are useful to us here.  First, choice weightings can use one-time rules or embedded Javascript to determine the weighting.  Second, a weighting of zero means a choice will never be chosen.  So to force a particular number of points to be drawn, I need to set the weights of the other choices to zero.  For example, this rule:<topLayer> =>  <cardinalPoints> [0] |
<interCardinalPoints> <cardinalPoints> [1] |
<interOrdinalPoints> <interCardinalPoints> <cardinalPoints> [0]
will always draw two levels of points, since the other two options are set to zero.  Now I need to modify this rule so that a weight of zero or one is selected for each option based upon $pointLevels:<topLayer> => <cardinalPoints> [$pointLevels == 1 ? 1 : 0] |
<interCardinalPoints> <cardinalPoints>
[$pointLevels == 2 ? 1 : 0] | <interOrdinalPoints> <interCardinalPoints> <cardinalPoints> [$pointLevels == 3 ? 1 : 0];The embedded Javascript uses the ternary operator to check if $pointLevels is set to the corresponding level, and returns 1 if so and otherwise 0. So the weight for the proper choice ends up being 1 and the other choices zero.I also need to set up the proper number of radial elements based upon the points level. I'll do that at the same time I pick the point level:$pointLevels => 1 @numInsideRadialElements=Utils.randElement([4,8,12,16,20,24,28,32]),"" |
2 @numInsideRadialElements=Utils.randElement([8,16,24,32]),"" |
3 @numInsideRadialElements=Utils.randElement([16,32]),"" ;I also need to set some default values for the number of radial elements and the offset of the elements.  These are the values that will be used in any outer ring of radial elements, where we don't have to worry about clashing with the compass points:$numRadialElements => 8 | 12 | 16 | 20 | 24 | 28 | 32;$offsetRadialElements => 0;
The last part of the puzzle is to replace these default values before I create any inner ring of radial elements.  <bottomLayer> => SPACE(Utils.randIntRange(10,30))
<ringOrScale> REMEMBER("insideEdge") |
SPACE(Utils.randIntRange(5,10))
<ringOrScale> REMEMBER("insideEdge")
@numRadialElements = $numInsideRadialElements, "" @offsetRadialElements = Math.PI/$numInsideRadialElements, ""
SPACE(10) <ring>;
This is the rule that decides whether the bottom layer will have only an outer ring (the first choice) or an outer and an inner ring (the second choice).  In the second choice, the embedded Javascript sets the number of radial elements to the value that was selected back in the $pointLevels rule, and then uses that to calculate an offset.Here's an example where there are 20 elements in the outside ring with no offset and eight elements in the inside ring with an offset that centers the first element between the first two compass points.I'm still not entirely happy with how compasses like the above example look where a point crosses a dark background element. One way to address this might be to add a white border around the point to create a visual break with the background. Here's an example of what this would look like:If you examine where the points cross background objects you'll see there's now a thin white line separating the point. This is done by drawing the outline of the point in white, using a thicker line, before drawing the point on top. The result is a white line around the point. However, if you draw this all the way around the point you get some unattractive artifacts where the points meet, as is evident in this example:If you only draw to the widest parts of the point there is still a problem:The white line for the top points separates them in an awkward way from the lower points. The solution is to pull the midpoints in slightly for the white line, so that it tapers down to nothing at the midpoints. This is easier to see if I draw the lines in red:The separation is fat out at the tip of the point and diminishes to nothing at the midpoint. A similar technique can be applied to wavy points as well. I don't think this is a “traditional" compass-drawing technique, and it will be problematic once more colors are possible, so I'll leave it in for now as an option. (But see below for controlling the option from CDL.)A reminder that if you want to follow along from home, you need to download the Part 17 branch from the Procedural Map Compasses repository on Github and get the test web page open. 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 17 directory, and then select the test.html link. You can also try out this code on the web at https://dragonsabound-part17.netlify.app/test.html although you'll only be able to run the code, not modify it.Suggestions to ExploreI noted above that one-time non-terminal references should only be allowed within embedded Javascript. Can you modify the Lodestone grammar to enforce that?Add a command to CDL to control the white border option, and more generally for any other options that might arise. OPTION(name, ON/OFF/value) might be a suitable format. Monday, April 11, 2022 Map Compasses (Part 16): More Rings I'm back from watching the NCAA basketball tournament and a Spring Break trip to St. Lucia, so it's time to get back to my blog series on implementing procedurally-generated map compasses! Last time I added scales to the two-part compasses: There's still a lot more potential in the rings, so let's continue to work on those. One of the Suggestions to Explore I wrote last time was to implement some rings with radial elements, as in this example compass: You can see three rings with radial elements in this example, but the general idea is pretty clear: a ring of radial elements, optionally enclosed in circles or on a dark circle. As usual, I'll start with a very simple version: # Radial element rings <radialRing> => <radialElementsOnly>; <radialElementsOnly> => RCIRCLE(0, 32, 2, 0.5, "black", "white"); You can refresh your memory on the RCIRCLE command by looking at cdl.ne, but the arguments are the starting point, the number of circles, the radius and the line width and colors. This just draws a ring of small circles as so: This looks fine, but I don't like the look when intercardinal points go over the ring: which I didn't love when it happened to the other sorts of rings either, so I'm going to go back and tweak the spacing for the start of the intercardinal points to keep that from happening. When there's a second ring in the compass, the radial elements are touching the second ring: This happens because radial elements don't advance the cursor. I need to do that manually. <radialElementsOnly> => RCIRCLE(0, 32, 2, 0.5, "black", "white") SPACE(2); Now I'll do some Level 1 elaboration on the number of circles, size, line width, etc. I won't walk through it in detail, but this is what I ended up with: # Radial element rings <radialRing> => <radialElementsOnly> ; <$relementSize> => Utils.randRange(2,5);
<relementLineWidth> => 0.5 | 1;
<numRadialElements> => 16 | 24 | 32 | 40;
<radialElementsOnly> => RCIRCLE(0, <numRadialElements>, <$relementSize>/2, <relementLineWidth>, "black", "white") SPACE(<$relementSize>/2);

I found that I only liked multiples of 8 for the number of elements, but you can experiment with other values and see what you think.  (The reason I'm using half the <$relementSize> here is that RCIRCLE takes a radius rather than a diameter like the other radial elements.) Now I'll do the obvious Level 4 elaboration and add the other kinds of radial elements: <radialElementsOnly> => SPACE(<$relementSize>)
RCIRCLE(0, <numRadialElements>, <$relementSize>, <relementLineWidth>, "black", "white") SPACE(<$relementSize>) |
SPACE(<$relementSize>) RTRI(0, <numRadialElements>, -<$relementSize>, <$relementSize>, <relementLineWidth>, "black", "white") | SPACE(<$relementSize>)
RTRI(0, <numRadialElements>, -<$relementSize>, <$relementSize>,
<relementLineWidth>, "black", "white", "black") |
SPACE(<$relementSize>) RDIAMOND(0, <numRadialElements>, -<$relementSize>, <$relementSize>, <relementLineWidth>, "black", "white") | SPACE(<$relementSize>)
RDIAMOND(0, <numRadialElements>, -<$relementSize>, <$relementSize>,
<relementLineWidth>, "black", "white", "black") |
SPACE(<$relementSize>) RCIRCLE(0, <numRadialElements>, <$relementSize>,
<relementLineWidth>, "black", "black")
SPACE(<$relementSize>) | SPACE(<$relementSize>)
RTRI(0, <numRadialElements>, -<$relementSize>, <$relementSize>,
<relementLineWidth>, "black", "black") |
SPACE(<$relementSize>) RDIAMOND(0, <numRadialElements>, -<$relementSize>, <$relementSize>, <relementLineWidth>, "black", "black"); The only thing that's a little tricky here is getting the spacing right. RCIRCLE is centered on the radius, so a half move before and after leaves the circles right at the radius. But RTRI and RDIAMOND are centered at the bottom of the element, so when I'm using a negative height (as I am here to make sure the triangle points outward) then I need to do the full space before drawing. Since I've done the all-white and the half-white/half-black versions, I might as well throw in the all black versions as well. That's pretty good for radial elements only, so now I'll work on the other variants. The first is to frame the radial elements in two circles: <radialElementsFramed> => <thinCircle> SPACE(<$relementSize>/2)
SPACE(<$relementSize>/2) <thinCircle>;  That's just drawing the radial elements centered between two circles. I didn't include radial lines in the stand-along radial elements because I didn't think they would look good, but I can include them here between two circles. This can end up looking something like a scale. The drawing of this is a little different because I want the line to connect to the framing circles. It took me a few tries to get that right, and in one of those tries I was short of the inner circle: That looks okay to me so I kept it. I find this happens fairly frequently -- I'm debugging the code and inadvertently stumble across something I hadn't been intending but like anyway. And I might as well include doubled circles as I did for scales: <$frameCircle> => <thinCircle> [2] | <thinCircle> SPACE(<$thinWidth>) <thinCircle>; <radialElementsFramed> => <$frameCircle>
SPACE(<$relementSize>/2) <radialElementsPlus> SPACE(<$relementSize>/2)
<$frameCircle>; Note that I did the trick of using a one-time rule to make sure the doubled circles on each side of the radial elements are identical. Another variation I can try for radial elements is to alternate elements, by creating a ring that is (say) alternating circles and triangles. To do this I need to draw two sets of radial elements with half the number, and one of them offset so that it falls between the others. This also often happens during the development of procedural generation -- an elaboration you applied elsewhere easily applies to some new piece as well. As you work, your pace of development increases as you leverage what you've done before. It's also a way to develop a “style" -- for example, if I used these doubled lines throughout the procedural generation, you'd likely start to recognize that as indicative of this generator. Now I'll add in the other variant with a dark background. This is kind of interesting because I have to draw the radial elements in white so that they show up on the background. <radialElementsDark> => CIRCLE(<$relementSize>*2, "black", "none")
SPACE(-3*<$relementSize>/2) <radialElementsReversed> SPACE(<$relementSize>/2) |
<thinCircle> SPACE(<$thinWidth>) CIRCLE(<$relementSize>*2, "black", "none")
SPACE(<$thinWidth>) <thinCircle> SPACE(-3*<$relementSize>/2-<$thinWidth>*2) <radialElementsReversed> SPACE(<$relementSize>/2 + <$thinWidth>*2); <radialElementsReversed> => SPACE(<$relementSize>)
RCIRCLE(0, <numRadialElements>, <$relementSize>, <relementLineWidth>, "white", "white") | SPACE(<$relementSize>)
RCIRCLE(0, <numRadialElements>, <$relementSize>, <relementLineWidth>, "white", "black") | SPACE(<$relementSize>)
RTRI(0, <numRadialElements>, -<$relementSize>, <$relementSize>,
<relementLineWidth>, "white", "white") |
SPACE(<$relementSize>) RTRI(0, <numRadialElements>, -<$relementSize>, <$relementSize>, <relementLineWidth>, "white", "black") | SPACE(<$relementSize>)
RTRI(0, <numRadialElements>, -<$relementSize>, <$relementSize>,
<relementLineWidth>, "white", "white", "black") |
SPACE(<$relementSize>) RDIAMOND(0, <numRadialElements>, -<$relementSize>, <$relementSize>, <relementLineWidth>, "white", "white") | SPACE(<$relementSize>)
RDIAMOND(0, <numRadialElements>, -<$relementSize>, <$relementSize>,
<relementLineWidth>, "white", "black") |
SPACE(<$relementSize>) RDIAMOND(0, <numRadialElements>, -<$relementSize>, <$relementSize>, <relementLineWidth>, "white", "white", "black"); Here I'm centering the radial elements on a dark circle twice the width of the element. I'm not sure if I like having a dark side to triangles and diamonds in this scheme: But I don't hate it either, so I'll leave it in until I do. I might as well throw in the double line style here as well. You can see the rule for this in compass.rules. Another elaboration I can do for radial elements is to alternate elements, e.g., make a ring that alternates circles and triangles. I don't have an example compass that does this, but I'm thinking of something like this: There are a couple of tricky aspects to this elaboration. First, the radial elements are potentially of different sizes and I want them both to be centered at the same distance from the center of the compass. Second, ideally I'd like the two elements to be different, although it would be acceptable if they were the same. It's difficult/impossible in a context-free grammar to implement “pick this randomly but make sure it isn't the same as this other thing." In this case, I could do it by writing out rules for all the specific cases, e.g., something like: <alternating> => <diamond> <firstDiamond> | <circle> <firstCircle> | <triangle> <firstTriangle> <firstDiamond> => <circle> | <triangle> <firstCircle> => <diamond> | <triangle> <firstTriangle> => <circle> | <diamond> Where I limit my choices for the second element based upon what I had chosen for the first element. Of course, this gets cumbersome as you get more and more choices. In this case, I decided to do something different. I made the first element chosen small and the second element chosen larger. This way, even if I happen to get the same elements for both, there will still be a visual difference: I chose this partially because this also helps solve the second problem. Since I know the second element is always the biggest element, I can use the size of that element to center both elements. Here's an edited version of the rules: <radialElementsAlternating> => <radialElementsFirst> <radialElementsSecond>; <$numAlternatingElements> => 8 | 12 | 16 | 20;
<$relementFirstSize> => Utils.randRange(4,5.5); <$relementSecondSize> => Utils.randRange(5.5,7);
# Firsts are smaller than seconds, so they need to center in <$relementSecondSize> <radialElementsFirst> => SPACE(<$relementSecondSize>/2+<$relementFirstSize>/2) RCIRCLE(0, <$numAlternatingElements>, <$relementFirstSize>, <relementLineWidth>, "black", "white") SPACE(-<$relementSecondSize>/2+<$relementFirstSize>/2) | ... ; <radialElementsSecond> => SPACE(<$relementSecondSize>)
RCIRCLE(Math.PI/<$numAlternatingElements>, <$numAlternatingElements>, <$relementSecondSize>, <relementLineWidth>, "black", "white") | ... ; First I have some one-time rules that select the sizes of the elements, and I pick ranges so that the second element will always be larger. Then I draw the first elements, moving in (and then back out) to the correct spot to center inside a ring the size of the second element. Then I draw the second element the same way (although I don't have to move back out). I've only included the options for RCIRCLE here but you can see the full rule in compass.rules. Another variant type of ring occurs to me -- one where the radial elements are strung out on a circle like beads. I don't have an example compass that does this, but the idea is straightforward. Like the reversed colors variant above, I'll put a circle down behind a ring of radial elements. But instead of a fat circle that fills up the ring's width, I'll do a thin one. <radialElementsNecklace> => SPACE(<$relementSize>/2+0.5)
CIRCLE(1, "black", "none")
SPACE(-(<$relementSize>/2+0.5)) <radialElementsPlus>  And of course I might as well do this with alternating elements and the other variants as well, which gives me this lovely example: With shaded diamonds differing just a bit in size. And of course I will add my signature double line style: Lastly, inspired by the ornaments on the directional pointers on this compass: I'll add a new radial element composed of radial circles and diamonds together. The tricky thing here will be getting the sizes and placement of all the pieces correct. I'll want this to act like a single element of the specified size, which will mean adjusting the sizes of the pieces and placing them accordingly. To start with, I'll write a rule to place radial circles as normal, but I'll only make them half the size of the full element. Since the bottom of the element will remain the same they'll be in the bottom half of the space for the full element:  SPACE(<$relementSize>)
RCIRCLE(0, <numRadialElements>, <$relementSize>/2, <relementLineWidth>, "black", "white");  Here I've framed the space so you can see the placement. Now I need to put a triangle in to fill up the rest of the space for the element, with the bottom of the triangle at the widest part of the circle. If you do the math, this means I have to move the triangle up 1/4 of the element size, and make the length of the triangle 3/4 the element size, and the base of the triangle 1/2 the element size (so that it is obscured behind the circle).  SPACE(3*<$relementSize>/4)
RTRI(0, $numRadialElementsShared, -3*<$relementSize>/4.1, <$relementSize>/2.2,$relementLineWidthShared, "black", "black")
SPACE(<$relementSize>/4) RCIRCLE(0,$numRadialElementsShared, <$relementSize>/2,$relementLineWidthShared, "black", "black");

Notice in the rule above I created one-time values for the number of elements and the line width.  Without that, those might mismatch between the circle and the triangle.  You'll also notice I actually made the triangle dimensions a little smaller than needed to avoid the edges of the triangle from peeking out from behind the circle.  My radial elements are much smaller than in the example compass.  All black as above works okay, but white and black as in the example compass is really too small to see clearly:
So I'll just leave this black.  Here's another multi-part radial element that works better at this scale:
	SPACE(<$relementSize>) RCIRCLE(0,$numRadialElementsShared, <$relementSize>, 0.5, "black", "white") RLINE(0,$numRadialElementsShared, -<\$relementSize>, 0.5, "black");

A reminder that if you want to follow along from home, you need to download the Part 16 branch from the Procedural Map Compasses repository on Github and get the test web page open.  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 16 directory, and then select the test.html link.  You can also try out this code on the web at https://dragonsabound-part16.netlify.app/test.html although you'll only be able to run the code, not modify it.  Still, it's fun to try it out a few times and see what you get!

Suggestions to Explore

• I've implemented two “pseudo" radial elements created by composing two of the existing elements.  Invent some additional pseudo elements and implement them.

• An interesting variant of a radial element is a circular repeating pattern as in this example:

Or more simply, a line that zig-zags up and down to make sawteeth.  How would you implement these patterns?