Tuesday, March 17, 2020

Making an SVG shape specification easy to interpret visually.

In my most recent post, I deferred describing how I parsed an SVG document to another post.

There are multiple subtopics:

  1. How to make the SVG easy to interpret visually.
  2. How to convert the SVG into a set of rules.
  3. How to select the right rules given a query string.
  4. Generating an SVG that explains what went wrong when a test fails.

I will attempt to address them all in separate posts. Like the rest of the world (at the time of this writing), I'm recovering from illness. So I'll do an easy one, now, and the rest will have to wait.

First up, how to make the SVG easy for a person to understand.

This part is all about SVG, itself. I started out with a single document that looked pretty raw - just some white rectangles on a black background. Over time I accumulated more documents and evolved them into something more palatable.

Those mostly involved the use of stylesheets and definitions but also, after much experimentation, I discovered that polyline was the most effective tool to create the shapes I wanted. I'll explain why in a bit.

First, let's look at a single polyline element:

<polyline id=".inner" points="80,192 80,1000 1860,1000 1860,192 80,192 80,193" />

That's a rectangle with its first two vertices repeated. For the test, I only need the first three points - the rest of the parallelogram is inferred.

However, to create the visual effect of a bounding box with little arrowheads pointing inward at the corners, I needed the extra points. At least, I couldn't figure out how to do it without the extra points.

I could only get the orientation of markers on inner vertices to be correct. Everything else pretty much just looked like a random direction had been chosen. As a result, I needed 4 inner vertices, which means I needed six of them, total (start, inner x 4, end).

The other structures I needed were some defined shapes to use as vertex-markers.

<defs>
    <marker id="marker-outer" viewBox="0 0 10 10" refX="5" refY="10"
        markerWidth="5" markerHeight="5"
        orient="auto">
        <path d="M 5 10 L 2 0 L 8 0 z" class="label" />
    </marker>
    <marker id="marker-inner" viewBox="0 0 10 10" refX="5" refY="0"
        markerWidth="5" markerHeight="5"
        orient="auto">
        <path d="M 5 0 L 2 10 L 8 10 z" class="label" />
    </marker>
</defs>

Once I have a rule-definition (my polyline, in this case) and the definition of the marker, I can use a stylesheet to marry the two and create the visual effect.

<style>
    *[id='inner'],
    *[id$='.inner'],
    *[id='outer'],
    *[id$='.outer']
    {
        stroke-width: 5;
        stroke: white;
        stroke-dasharray: 30 5;
        fill:none;
    }

    *[id='inner'],
    *[id$='.inner']
    {
        marker-mid: url(#marker-inner);
    }

    *[id='outer'],
    *[id$='.outer']
    {
        marker-mid: url(#marker-outer);
    }

    /* SNIPPED: Stuff used at runtime for other purposes */
</style>

Finally, to create a frame of reference, I converted a background image to base 64 and embedded it in the SVG document as the first element.

All of those steps create an effect like this:


Thankfully, most of those steps don't need to be repeated.

Sadly, it seems that SVG pushes you in the direction of redundancy. You can externalize your stylesheet but not every renderer will respect it. I couldn't find a way to reliably reuse the markers, either. The background image could be externalized but then I'd be relying on hosting for the specification to render properly.

There's a bunch of copy and paste but it's not on the critical path for the test. It just affects how the test looks to developers. So I tolerate it.

I could write a little generator that runs just before build time but then I wouldn't be able to easily preview my work.

C'est la vie.

At least, this way, I can quickly interpret my specification just by opening it in a browser or editor.