JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

SVG Group II

Aug 14, 2025 • [designlinkagesluamoverstesting]


Behold the awesome power of … POLYMORPHISM!!!

When I write these articles, time goes roughly as the articles do: I say something about what I’m going to do, I try to do it, and then I say something about what actually happened. Yesterday was no exception, except for that last part.

Somehow, my work led me to try a test of two linkage diagrams on the same page, as an exercise for the new group tag I’m adding. It didn’t work and after a while I recognized why that was. Here’s a snippet of the XML that shows the issue:

<g fill="none" stroke="none" opacity="1" 
transform="translate(0, 200)" />
<circle cx="10" cy="2" r="2" fill="none" stroke="red" opacity="1" />
<line x1="10" y1="2" x2="10" y2="0" fill="none" stroke="yellow" opacity="1" />
<line x1="10" y1="1" x2="16" y2="1" fill="none" stroke="green" opacity="1" />
</g>

Take a close look at the initial g tag. You may notice right away, but it took me a long time to recognize that the tag is closed. Therefore it has no effect on what follows. And that is not good. We have to fix that.

The tag is created in the SVG library, in this method:

function Shape:_SVG(tagname, content)
    if content then
        return `<{tagname} {self:_attributes()}>{content}</{tagname}>`
    else
        return `<{tagname} {self:_attributes()} />`
    end
end

By design, each tag the SVG library creates is immediately closed. In the case of the group tag, we want that trailing slash in the second return not to appear.

We could do that by passing in a flag, or with some kind of check for the tagname. I propose to do it more cleanly, with, as mentioned above, the awesome power of polymorphism.

Polymorphism
As all here surely know, polymorphism is an aspect of object-oriented programming that allows different objects to implement the same method name, and to perform that method differently.

Polymorphism is already used in the SVG library: every Shape subclass implements tostring, and does so just a bit differently. They also implement new differently. I propose to provide a new method and override it in just one Shape subclass, Group. Watch this:

function Shape:_SVG(tagname, content)
    if content then
        return `<{tagname} {self:_attributes()}>{content}</{tagname}>`
    else
        return `<{tagname} {self:_attributes()} />`
    end
end

I extract a new method _SVG_tail that computes the final bit on the second branch of the if:

function Shape:_SVG(tagname, content)
    if content then
        return `<{tagname} {self:_attributes()}>{content}</{tagname}>`
    else
        return `<{tagname} {self:_attributes()} ` .. self._SVG_tail()
    end
end

function Shape:_SVG_tail()
    return '/>'
end

This should behave just as before. Based on my tests, it does.

Now we override that method in Group:

function Group:_SVG_tail()
    return '>'
end

Now, I expect the trailing slash to be gone in my group tags:

<g fill="none" stroke="none" opacity="1" 
transform="translate(0, 200)" >
<circle cx="10" cy="2" r="2" fill="none" stroke="red" opacity="1" />
<line x1="10" y1="2" x2="10" y2="0" fill="none" stroke="yellow" opacity="1" />
<line x1="10" y1="1" x2="16" y2="1" fill="none" stroke="green" opacity="1" />
</g>

And it is. Now I should be able to get two pictures on the same background. Hold on, this may take a moment.

The first thing that happens is that NO objects appear in the picture. This does not please me, as you can imagine. However, I have a hacked-in transform that should be removed, as a start.

After more grotesque hackery than I’d like to admit, I have this test:

_:test("first drawing", function()
    local rect = svg.Rect{x=0, y=0, width=64, 
        height=48, fill=0x191919}
    rect:translate(0,-48)
    local group = svg.Group(function()
        local wheel = DriveWheel(10, 2, 2)
        local wheel_circ = 2*math.pi*2
        wheel:move(wheel_circ/8) -- 45 == pi/4
        local rod = CouplingRod(6, wheel, 1)
        wheel:to_svg()
        rod:to_svg()
    end)
    group:translate(0, -12)

    group = svg.Group(function()
        local wheel = DriveWheel(10, 2, 2)
        local wheel_circ = 2*math.pi*2
        wheel:move(wheel_circ/4) 
        local rod = CouplingRod(6, wheel, 1)
        wheel:to_svg()
        rod:to_svg()
    end)
    group:translate(0, -36)
    local s = svg.Shape:draw()
    print(s)
end)

It produces this drawing:

What’s at issue here is that I have the coordinate systems confused in my mind. What I want to have is for things to draw such that the y axis increases from the bottom of the picture to the top.

Stymied!

After inching forward, step by step, working out the transforms that I need, I’ve run into something that I don’t like and don’t se a good way around: the SVG library defaults values that you don’t provide, and, in particular, defaults stroke-width to 1. So even though I override it to 0.1 in my group, any SVG object where I don’t provide a stroke width gets 1, overriding the group’s value.

I can’t just stop providing it, because then when I set it in a group definition it won’t show up.

OK, maybe I can check it for presence and use it only then. This is a hack and it will surely bite me.

Ugly but it works. I am back on track.

Progressing. I wonder if my Groups can be nested. (I am very close to deciding to provide a group begin and group end and using them explicitly rather than with the nested function thing. After all, I’m just trying to draw some debugging pictures here not spend days doing graphics.)

OK, I have success, after a fashion. I’ve removed the spliced-in group that I had in SVG, and wrote this small test:

_:test("basic drawing", function()
    local rect = svg.Rect{x=0, y=0, width=640, 
        height=480, fill=0x191919}
        local g1 = svg.Group({stroke_width=0.1}, function()
            local line = svg.Line{x1=0,y1=0, x2=32,y2=24, stroke="green"}
            local g2 = svg.Group({}, function()
                svg.Line{x1=0,y1=0, x2=32,y2=24, stroke="red"}
            end)
            g2:translate(0,10)
        end)
        g1:scale(10,-10)
        g1:translate(0, -48)
    local s = svg.Shape:draw()
    print(s)
end)

What this should do is draw a green line from 0,0 to 32,24 on a screen that, via group g1, is scaled 10,-10 and translated -48 down. Then, inside a second group g2, translated upward by ten, draw a red line from 0,0 to 32,24, which should result in a green line from the lower left corner upward to the center of the 64x48 scaled screen, and then a red line from about a quarter of the way up, parallel to the green.

And that is what I get:

So, good. I’ve backed way up to making sure the basic coordinates act as I wish. The issue was complicated by the order of application of transforms, and by the SVG’s library reasonable decision to default things like stroke-width, because it didn’t contemplate a group object, which I have wedged in.

I’ve been at this for two hours and that is plenty. Time for a break.

Reflection

I have implemented graphics routines like these many times, and every time it has come down to needing to test very rudimentary drawings to get it right. It is probably possible for someone to work out all the implications in their head or on paper, but I am not that someone and for me, it always comes down to a bit of detailed trial and error.

This was made harder because SVG isn’t under the control of the library and SVG does whatever it does, which I needed to discover by trial. And the SVG library is the same: I needed to learn what it does by trial. The good news there is that I am able to change it as needed.

The library didn’t expect a group object, so I did have to whack it a bit hard in a couple of places to get that to work.

But work it does. I think that next time, I can get back to drawing pictures of my linkages. And then, I can get back to calculating linkages.

Someone said it’s yaks all the way down, and that sure seems true today.

Safe paths!