JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Linkages II

Sep 11, 2025 • [designlinkagesluatesting]


Let’s start with a clean, um, slate and build up some linkage code again, from scratch, whatever scratch is.

I begin with my standard header but forget that I have a file-starter saved, so I did it over. Go, me!

Here’s my tentative plan from yesterday:

  1. Set this points file aside;
  2. Set the previous linkages file aside;
  3. Start a new linkages project;
  4. Focus on components;
  5. Give them a standard root protocol;
  6. All driven points are in a dictionary by name;
  7. Driven points are defined with constraint thinking;
  8. Perhaps as objects;
  9. Perhaps as functions;
  10. Devise a better way to visualize linkages;
  11. Be prepared to branch back to #1.

Super, we’re already on step 4!

Last time, I started with the wheel. I think this time I’ll start with the crank just because the wheel seems odd. And as a general rule, I want to do things a bit differently this time around, so as to give myself a better chance to have new and better ideas.

_:describe("crank", function()
    _:test("exists", function()
        local pos = vector(2,3)
        local parent = {pos=pos}
        local crank = Crank{parent=parent,radius=1}
    end)
end)

I ran into a need for Wheel or something like it. For now, I’ve just rigged up a simple table with a value pos. I don’t think that can last: we need a function to return position, since in general the position of a component changes. We’ll go with it for now.

No, having thought about it, I’ll fix it right now:

_:test("exists", function()
    local pos = vector(2,3)
    local parent = {position=function(self) return pos end}
    local crank = Crank{parent=parent,radius=1}
end)

Might as well try to stay close to the design I see, while remaining open to the certainty that I don’t see it clearly.

My test passes with this code:

local Crank = class()
function Crank:init(parms)
    self.parent = parms.parent or error('expected parent')
    self.radius = parms.radius or error('expected radius')
end

The crank needs to have an angle so that its rotating end can have a position. And it needs a constant internal angle as well, indicating the lead or lag relative to the angle of the crank itself. For now, we’ll give it an angle-setting function.

This brings me to a concern. I was reading some Lua / Slua / Luau code yesterday and was reminded that the convention is to use camelCase for method names, not the snake_case that I’ve been preferring. Should I follow the herd here, or not?

We’ll try camelCase here, and see what we think about it.

local crank
_:setup(function()
    local pos = vector(2,3)
    local parent = {position=function(self) return pos end}
    crank = Crank{parent=parent,radius=1}
end)

_:test("driven end", function()
    crank:setAngle(0)
    _:expect(crank.movingEnd()).is(vector(3,3))
end)

I can see it is going to take me some time to move to camel case: I typed moving_end above, right after typing setAngle.

I’m struggling a bit here, trying to keep in mind that I want to give due consideration to the notion of constraints and functions, not just methods. We’ll see how it goes.

What about the angle? Should that be some kind of dynamic lookup as well? Should it perhaps be a parameter on movingEnd?

To answer these questions, I think I’ll need to set up a longer linkage, on paper, in code, and probably both. For now, we’ll just wander.

I did wander a bit, including adding a camel case method to vector. Here’s the test and code:

local crank
_:setup(function()
    local pos = vector(2,3)
    local parent = {position=function(self) return pos end}
    crank = Crank{parent=parent,radius=1}
end)

_:test("driven end", function()
    crank:setAngleDegrees(0)
    _:expect(crank:movingEnd()).is(vector(3,3))
end)


local Crank = class()
function Crank:init(parms)
    self.parent = parms.parent or error('expected parent')
    self.radius = parms.radius or error('expected radius')
    self:setAngleDegrees(parms.degrees or 0)
end

function Crank:setAngleDegrees(degrees)
    self.radians = degrees*DEG_TO_RAD
end

function Crank:movingEnd()
    return self.parent:position() + vector(self.radius, 0):rotate2d(self.radians)
end

We’ve left open how the Crank gets its angle. Nor are we dealing with the lead-lag angle. New test, enhance the code.

_:test('lead angle', function()
    crank = Crank{parent=parent, leadDegrees=45, radius=1}
    local s22 = math.sqrt(2)/2
    local movingEnd = crank:movingEnd()
    _:expect(movingEnd.x).is(2+s22, 0.001)
    _:expect(movingEnd.y).is(3+s22, 0,001)
end)

local Crank = class()
function Crank:init(parms)
    self.parent = parms.parent or error('expected parent')
    self.radius = parms.radius or error('expected radius')
    self.leadRadians = (parms.leadDegrees or 0)*DEG_TO_RAD
    self:setAngleDegrees(parms.degrees or 0)
end

function Crank:setAngleDegrees(degrees)
    self.radians = degrees*DEG_TO_RAD
end

function Crank:effectiveAngle()
    return self.radians + self.leadRadians
end

function Crank:movingEnd()
    return self.parent:position() + vector(self.radius, 0):rotate2d(self:effectiveAngle())
end

That’s passing. But I think we have to fetch the angle from the parent, like this:

local Crank = class()
function Crank:init(parms)
    self.parent = parms.parent or error('expected parent')
    self.radius = parms.radius or error('expected radius')
    self.leadRadians = (parms.leadDegrees or 0)*DEG_TO_RAD
end

function Crank:effectiveAngle()
    return self.parent:angleRadians() + self.leadRadians
end

function Crank:movingEnd()
    return self.parent:position() + vector(self.radius, 0):rotate2d(self:effectiveAngle())
end

This will fail all (2) tests. We need an angleRadians function in our parent.

Time to give up and create a Wheel, I think. I can just put it in and when the tests run again, it’ll be good.

local crank
local parent
_:setup(function()
    local pos = vector(2,3)
    parent = Wheel{position=pos}
    crank = Crank{parent=parent,radius=1}
end)

_:test("driven end", function()
    _:expect(crank:movingEnd()).is(vector(3,3))
end)

_:test('lead angle', function()
    crank = Crank{parent=parent, leadDegrees=45, radius=1}
    local s22 = math.sqrt(2)/2
    local movingEnd = crank:movingEnd()
    _:expect(movingEnd.x).is(2+s22, 0.001)
    _:expect(movingEnd.y).is(3+s22, 0,001)
end)

local Wheel = class()
function Wheel:init(parms)
    self.pos = parms.position or error('expected position')
    self.radians = (parms.angleDegrees or 0)*DEG_TO_RAD
end

function Wheel:setAngleDegrees(degrees)
    self.radians = degrees*DEG_TO_RAD
end

function Wheel:angleRadians()
    return self.radians
end

function Wheel:position()
    return self.pos
end

Let’s Review

And finally … this may all be moot. It may be that what we need is nothing, or a Python plugin for Blender, where most of our linkages will be developed. The days of a prim build for a linkage are over, and Blender has support for kinematics.

We’ll see. For now, may you find safe paths.