JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Linkages

Aug 11, 2025 • [linkagesluamoverstesting]


OK, moving right along, now comes the learning. (distance/radius doesn’t really count.) Let’s make a new linkage piece and make it connect.

We have a drive wheel, intended to be the base of our linkage, that knows its position, radius, and angle. A very simple linkage component is the coupling rod, which connects other driven wheels to the one we’ve identified as the drive wheel.

Aside
We are not simulating a real linkage here: we are calculating the positions and rotations of linkage elements so as to position them, making them appear to work. As such, we might not include driven wheels in our scheme, because our scripts might just turn them all directly, rather than work through the linkage code. We’ll see what seems best sooner or later.

Anyway, the coupling rod is connected to the driven wheel, at some radius from the center, which will be less than the wheel’s own radius. Our code will not need to rely on that fact, but a coupling rod at a radius larger than the wheel radius would bury itself underground.

The coupling rod, I believe, must always be horizontal, relative to the x-axis which the wheels align to. (We’re assuming x is parallel to the tracks and y is upward. Later implementations may consider z to be upward. For now, we’re working in the x-y plane.)

We need a test. I’ll try to write one, see how it goes. This is a design step, by the way, not a testing step. I’m designing how the CouplingRod will work, specifying that design in a test.

For now, I’ll put the tests in the same basic feature, but that may change as we move forward.

We’ll start with the assumption that the coupling rod is attached to the wheel at a given radius and at the wheel’s angle zero. We will quickly need to change that, because the wheels on opposite sides of a locomotive have their cranks attached 90 degrees out of phase. If they didn’t, it would be possible to get the locomotive stuck in an immobile state. I wonder who first worked that out and how much work they had to do to correct their mistake.

A coupling rod has some length, since it spans two or more wheels. We will assume, for now, that its center is its root. We can consider relaxing that constraint later.

I’m supposing we return a small table as position. We’ll surely make that a vector type in due time. Right now, I’m more concerned with the basic relationships among parts. Since the rod is of length 6 …

Note
I realize that I need to have a canonical picture of what’s going on here. I tend to think about the left side of a locomotive when I animate it and that makes the direction of motion be along negative x. Let’s not do that.

I’ll draw up a decent copy of the picture but we’ll have wheels of radius 2, centers 6 apart, and the drive wheel at x = 10. We’ll assume that angle 0 is pointing due east, three o’clock on an analog clock, if you are familiar with those.

_:test("coupling rod initial position", function()
    local wheel = DriveWheel(10, 2, 2) -- x, y, r
    local rod_radius = 1
    local rod_length = 6
    rod = CouplingRod(rod_length, wheel, rod_radius)
    rod:calculate()
    _:expect(rod:position()).is({13,2})
end)

I think that’ll do for a first test. We’ll refine as we go, as usual.

Test will fail for want of a class.

CouplingRod = class()
function CouplingRod:init(rod_length, wheel, rod_radius)
    self._length = rod_length
    self._parent = wheel
    self._radius = rod_radius
end

function CouplingRod:calculate()
end

function CouplingRod:position()
    return {}
end

Now it’ll fail for having the wrong answer. Yes.

I’m supposing, with the method calculate, that we’ll go through all the objects, telling them to calculate, and then go through again asking them what we need to know to move them.

So, where will position be when we calculate? The rod wants to know the angle of the wheel so as to calculate its position, I think like this:

function CouplingRod:calculate()
    local angle = self._parent:angle()
    local x, y = self._parent:position()
    local rod_end_x = self._radius*math.cos(angle)
    local rod_end_y = self._radius*math.sin(angle)
    self._position = {x + rod_end_x + self._length, y + rod_end_y}
end

I also think that the test is wrong, because the rod will be forward of the wheel center by 1. So I expect to fail with 14, not 13. Except that I expect some other problem to arise first. Like the wheel not having a position method yet.

function DriveWheel:position()
    return {self._x, self._y}
end

Also that means my x,y assignment isn’t going to work. Should be:

    local x, y = table.unpack(self.parent:position())

I think. Time to test and see what the computer thinks. It finds a syntax error, fixed in the code above. Test fails with 17, not the 14 I expected. Forgot to divide by 2:

CouplingRod = class()
function CouplingRod:init(rod_length, wheel, rod_radius)
    self._half_length = rod_length / 2
    self._parent = wheel
    self._radius = rod_radius
    self._position = {}
    self:calculate()
end

function CouplingRod:calculate()
    local angle = self._parent:angle()
    local x, y = table.unpack(self._parent:position())
    local rod_end_x = self._radius*math.cos(angle)
    local rod_end_y = self._radius*math.sin(angle)
    self._position = {x + rod_end_x + self._half_length, y + rod_end_y}
end

And I get the expected 14. Fix the test. Pass.

Another test. Move the drive wheel, get new answer. There is an issue with this: moving the right side wheel in the positive x direction rotates the wheel clockwise, that is, decreasing the angle. I fix the code:

function DriveWheel:move(distance)
    self:rotate_by( - distance / self._r)
end

Run the tests expecting failures but obvious values. Yes. Fix them. Commit this code.

Another couple of tests, to be sure this works, then we’ll assess how it’s shaping up so far.

_:test("coupling rod at -90", function()
    local wheel = DriveWheel(10, 2, 2) -- x, y, r
    local rod_radius = 1
    local rod_length = 6
    local rod = CouplingRod(rod_length, wheel, rod_radius)
    local wheel_circ = 2*math.pi*2
    wheel:move(wheel_circ/4) -- 90 == pi/2
    rod:calculate()
    _:expect(rod:position()).is({13, 1})
end)

The CouplingRod is at its lowest position, y = 1, and its center x is +3 past the wheel’s own center x. As intended.

I don’t feel the need for another test. Let’s think about what we have here.

Assessing

We’re getting closer to a reasonable model of things, looking at the right side of the locomotive, facing +x direction, with drive wheel angle starting at zero, due east, three o’clock like the old folks say.

We can adjust the wheel by a move method, or we could adjust it directly with rotate_by. It should have a calculate method, I suppose, that does nothing, because all the objects in the table we’ll be building should probably respond to calculate. I add an empty method.

Our new object CouplingRod, attaches by its low-x end to the DriveWheel at a provided radius out from wheel center. When the wheel angle changes, CouplingRod uses the well-known r cosine theta r sine theta formulas for that end’s x and y. It then adjusts that value by its half-length to get its center position, which will be the standard thing we answer, so that a linkage mover can just ask for positions and rotations and slam them in.

We have so far finessed both the notion of vectors, and the notion of rotations (quaternions), though we’ll surely have to deal with those in due time.

So what this is shaping up to be is a collection of various kinds of linkage components. Pistons and ConnectingRods will surely show up, and I’ll ask my sisters for other linkage problems. The theory is that any given component has references to the components that control its position, typically one or two others. They’ll be arranged from most free (DriveWheel) to less and less free (ankle bone connects to the foot bone, leg bone connects to the ankle bone kind of thing). We’ll adjust the bottom DriveWheels, then go through the table from bottom to top, calculating. Once we get to the top, each object will have positioned itself correctly.

Next time, we’ll look at the ConnectingRod that connects the piston to the DriveWheel. That will get us started on the object’s rotation, since the rotation of the ConnectingRod changes as it moves. It also has an interesting bit of math in its calculation, as discussed in yesterday’s article.

Feels good so far. I think this is going to turn out nice. There’s a lot of development and refactoring to do. First we’ll get a bit more in place. We already know how to do vectors and such.

Safe paths!