JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Linkages - use vector

Aug 15, 2025 • [linkagesluamoverstesting]


I’ve decided to use my vector class with the linkages. It should make things easier to express. We’ll find out.

It was a simple but slightly tedious refactoring to convert our classes to use vector. I decided to use my class, not the built-in vector of Luau, because mine can be extended to provide functions that we need. Luau’s is unfortunately not able to be extended.

I have all the linkage tests running again, with code that is better, because vectors, but not as good as it might be, because it was derived from open code and could use a little improvement. Here, for example, is CouplingRod, the rod that connects wheels together:

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 = vector(0,0)
    self:calculate()
end

function CouplingRod:calculate()
    local rotated =  vector(self._radius,0):rotate_2d(self._parent:angle())
    local rod_end = self._parent:position() + rotated
    self._position = rod_end + vector(self._half_length, 0)
end

function CouplingRod:position()
    return self._position
end

I’ve added rotate_2d to vector, and allow a vector constructor to omit the z coordinate, which will be set to zero. We’ll deal with any 3D issues later on. I chose to look up the algorithm for rotate_2d, which took much less time than deriving it would have. Standard sin cos sin cos stuff.

In the calculate function, there are two things not to like. Second, we are creating the vector(self._half_length, 0) on every calculate, and it is a constant. We should save the vector, not the half-length, like this:

(Yes, I know I didn’t say “First”, first.)

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

function CouplingRod:calculate()
    local rotated =  vector(self._radius,0):rotate_2d(self._parent:angle())
    local rod_end = self._parent:position() + rotated
    self._position = rod_end + self._half_length
end

Much nicer, I think. First, I don’t like creating the vector(self._radius,0) all the time, and I don’t like fetching the angle from the wheel to adjust it. The wheel should help us by providing the adjusted position based on its rotation.

Let me do it and then we’ll see what I mean and discuss the implications.

CouplingRod = class()
function CouplingRod:init(rod_length, wheel, rod_radius)
    self._half_length = vector(rod_length / 2, 0)
    self._parent = wheel
    self._offset = vector(rod_radius, 0)
    self._position = vector(0,0)
    self:calculate()
end

function CouplingRod:calculate()
    local rotated =  self._parent:adjusted_offset(self._offset)
    local rod_end = self._parent:position() + rotated
    self._position = rod_end + self._half_length
end

We save the raw offset of our pivot on the wheel, <rod_radius,0>, and we ask the wheel to adjust it.

In the wheel:

function DriveWheel:adjusted_offset(v)
    return v:rotate_2d(self._angle)
end

Any (local) point on the wheel will move as shown there, as the wheel rotates. Having discovered that this is useful, I suspect that we may find it useful to provide a similar function for other linkage pieces, or all of them. We’ll see.

I am still wondering whether the objects should retain knowledge of their ends (their pivot points) as well as their center. Again, we’ll see what the code wants.

Now let’s look at the ConnectingRod, which has also been converted to vector but could use a little improvement.

ConnectingRod = class()
function ConnectingRod:init(rod_length, parent, rod_radius)
    self._parent = parent
    self._length = rod_length 
    self._radius = rod_radius
    self._position = vector(0,0)
    self:calculate()
end

function ConnectingRod:calculate()
    base_pos = self._parent:position()
    local rotated = vector(self._radius,0):rotate_2d(self._parent:angle())
    local wheel_end = base_pos + rotated
    local delta_y = base_pos.y - wheel_end.y
    local delta_x = - math.sqrt(self._length*self._length - delta_y*delta_y)
    local delta = vector(delta_x, delta_y) 
    local center = wheel_end + delta/2
    self._position = center
end

function ConnectingRod:position()
    return self._position
end

Here, I’ll store the offset instead of radius, and use the new method:

ConnectingRod = class()
function ConnectingRod:init(rod_length, parent, rod_radius)
    self._parent = parent
    self._length = rod_length 
    self._offset = vector(rod_radius, 0)
    self._position = vector(0,0)
    self:calculate()
end

function ConnectingRod:calculate()
    base_pos = self._parent:position()
    local rotated = self._parent:adjusted_offset(self._offset)
    local wheel_end = base_pos + rotated
    local delta_y = base_pos.y - wheel_end.y
    local delta_x = - math.sqrt(self._length*self._length - delta_y*delta_y)
    local delta = vector(delta_x, delta_y) 
    local center = wheel_end + delta/2
    self._position = center
end

It may be worth mentioning that I made just that one two-line change and ran my tests. I could commit the code as working and improved, and probably should. Moving to the next opportunity …

Let’s try breaking out the calculation of delta into a separate method, since it is kind of complicated. And since we only use half of it, we’ll do that as well.

function ConnectingRod:calculate()
    base_pos = self._parent:position()
    local rotated = self._parent:adjusted_offset(self._offset)
    local wheel_end = base_pos + rotated
    local center = wheel_end + self:center_delta(base_pos, wheel_end)
    self._position = center
end

function ConnectingRod:center_delta(base_pos, wheel_end)
    local delta_y = base_pos.y - wheel_end.y
    local delta_x = - math.sqrt(self._length*self._length - delta_y*delta_y)
    return vector(delta_x, delta_y) / 2
end

Tests run. Looking at that we see we could have had a method center_position, so let’s do:

ConnectingRod = class()
function ConnectingRod:init(rod_length, parent, rod_radius)
    self._parent = parent
    self._length = rod_length 
    self._offset = vector(rod_radius, 0)
    self._position = vector(0,0)
    self:calculate()
end

function ConnectingRod:calculate()
    base_pos = self._parent:position()
    local rotated = self._parent:adjusted_offset(self._offset)
    local wheel_end = base_pos + rotated
    self._position = self:center_position(base_pos, wheel_end)
end

function ConnectingRod:center_position(base_pos, wheel_end)
    return wheel_end + self:center_delta(base_pos, wheel_end)
end

function ConnectingRod:center_delta(base_pos, wheel_end)
    local delta_y = base_pos.y - wheel_end.y
    local delta_x = - math.sqrt(self._length*self._length - delta_y*delta_y)
    return vector(delta_x, delta_y) / 2
end

Notice that I created a new method to get the center position, given the center delta which we already had. Yes, we could combine those two methods. I think it’s better code design not to, but it’s a judgment call. The best reason to inline it would be to save the function call but we can do rather a lot of those per unit time. I prefer this for clarity.

I think that will suffice for this afternoon. We’ve got our classes converted to a much stronger focus on vectors, though I wouldn’t swear we won’t find more ways to improve them. When we do, we will.

Safe paths!