JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Simpler MainRod!

Aug 27, 2025 • [designlinkagesluatesting]


I’ve been working quietly on a new MainRod. Finally getting somewhere. Here’s my report.

I reported last time on a test function that computed main rod endpoint positions, and on my trials to get it tested. I was trying to use the existing objects, MainRod and GeneralMainRod to test it, and I just couldn’t find a simple way to do it. So I decided to bear down and write direct tests that checked the right answers. Here they are so far:

function _:featureNewMainRod()
    _:describe("new main rod", function()

        local wheel
        local main_rod
        _:setup(function()
            wheel = DriveWheel{x=10, y=2}
            main_rod = NewMainRod{
                parent=wheel, 
                length=6,
                radius=1,
                tilt_angle=0}
        end)

        _:test("initial", function()
            local end_x = 1 + 6
            main_rod:calculate()
            _:expect(main_rod:end_position()).is(vector(10+end_x, 2))
        end)

        _:test("30", function()
            wheel:set_angle(30)
            _:expect(wheel:angle()).is(30, 0.1)
            main_rod:calculate()
            local x = math.sqrt(35.75)
            local r = math.sqrt(0.75)
            -- print(x, r, 10+x+r)
            local expected = vector(10+math.sqrt(35.75) + math.sqrt(0.75), 2)
            local result = main_rod:end_position()
            _:expect(result.x).is(expected.x, 0.001)
            _:expect(result.y).is(expected.y, 0.001)
        end)

        _:test("150", function()
            wheel:set_angle(150)
            _:expect(wheel:angle()).is(150, 0.1)
            main_rod:calculate()
            local x = math.sqrt(35.75)
            local r = math.sqrt(0.75)
            -- print(x, r, 10+x+r)
            local expected = vector(10+math.sqrt(35.75) - math.sqrt(0.75), 2)
            local result = main_rod:end_position()
            _:expect(result.x).is(expected.x, 0.001)
            _:expect(result.y).is(expected.y, 0.001)
        end)

        _:test("45", function()
            wheel:set_angle(45)
            main_rod:calculate()
            local expected = vector(10+math.sqrt(35.5) + math.sqrt(2)/2, 2)
            local result = main_rod:end_position()
            _:expect(result.x).is(expected.x, 0.001)
            _:expect(result.y).is(expected.y, 0.001)
        end)

        _:test("270", function()
            wheel:set_angle(270)
            main_rod:calculate()
            local expected = vector(10+math.sqrt(35),2)
            _:expect(main_rod:end_position()).is(expected)
        end)
    end)
end

I hand calculated all those results on paper, doing the quadratic formula and my calculator app to get the numbers. And so far, the code is passing.

The object itself is designed to be able to deal with a piston rod at any angle to the drive wheel. The tests we see above are all testing the horizontal inclination only.

Here’s the object so far:

NewMainRod = class()
function NewMainRod:init(parms)
    self._parent = parms.parent or error('expected parent')
    self._length = parms.length or error('expected length')
    self._radius = parms.radius or error('expected radius')
    self._tilt_angle = parms.tilt_angle or error('expected tilt_angle')
    self._start_position = vector(0,0)
    self._end_position = vector(0,0)
end

function NewMainRod:calculate()
    local origin = self._parent:position()
    self._end_position = self:compute_end_position(
        self._parent, 
        self._tilt_angle,
        self._length,
        self._radius)
end

function NewMainRod:end_position()
    return self._end_position
end

function NewMainRod:position()
    return (self._start_position + self._end_position) / 2
end

function NewMainRod:compute_end_position(wheel, tilt_angle, length, radius)
    local adjust_angle = tilt_angle*DEG_TO_RAD
    local wheel_angle = wheel:angle()*DEG_TO_RAD - adjust_angle
    local x = radius*math.cos(wheel_angle)
    local y = radius*math.sin(wheel_angle)
    self._start_position = self._parent:position() + vector(x,y)
    local x_proj_sq = length*length - y*y
    local x_proj = math.sqrt(x_proj_sq)
    local result_x = x + x_proj
    local unrotated = vector(result_x, 0)
    local rotated = unrotated:rotate_2d(adjust_angle)
    return rotated + self._parent:position()
end

Now let’s test a non-zero tilt angle. I am quite sure that it works, from when I built the original prototype function, but “quite sure” isn’t good enough.

The fundamental idea of the implementation is this: if the angle of the piston rod is non-zero, the numeric results are the same as if the wheel rotation were reduced by the amount of the tilt. So we follow this scheme:

  1. Adjust the wheel rotation by the negative of tilt;
  2. Compute the horizontal figures;
  3. Rotate the horizontal result by the tilt.

That’s what compute_end_position is doing. I think that is not obvious, and we should make it more so. But first let’s do some testing.

    _:describe("new main rod tilted", function()

        local wheel
        local main_rod
        _:setup(function()
            wheel = DriveWheel{x=10, y=2}
            main_rod = NewMainRod{
                parent=wheel, 
                length=6,
                radius=1,
                tilt_angle=90}
        end)

        _:test("initial tilted 90", function()
            local expected = vector(10, 2+math.sqrt(35))
            main_rod:calculate()
            _:expect(main_rod:end_position()).is(expected)
        end)
    end)

Here we’ve configured a piston directly above the drive wheel, for my convenience in computation. At the initial angle 0, it should have the same result as a horizontal piston with a drive wheel at 270 (retarded by 90 degrees). Those figures are available from the paper examples I did, and from the code for the horizontal testing. The piston rod being vertical means that the x component of the rod end will not move: it sill remain at x=10. The radial offset is 1, so the distance in y is sqrt(36-1). We add that to our y origin, 2, and that’s our expected answer.

The test runs. I am satisfied, don’t even really want another test.

I’ll save what I have. The next step will be to remove the old MainRod and GeneralMainRod and use this one. That will just be a bit tedious because of the new creation sequence with tilt-angle. Otherwise it should be just fine.

What I’ll probably do is adjust the tests first, to use the new object, and then when they are all converted, there will be no references to the old classes and they can be removed. Those tests will also probably drive out some changes to the API for NewMainRod. We’ll see.

Safe paths!