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!