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:
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
I am very much wishing that the testing framework would let me include an error value when checking table entries (such as vector). I’ll take a look at that.
The transition to camelCase
seems smooth enough. We should probably stick with it, and anyway in LSL we use it as well.
I think that specifying Degrees
or Radians
on angles will be a good idea, since we will have degrees when building and need radians when computing.
I don’t like using pos
as the variable and position()
as the method. Too similar, not similar enough. Maybe coordinates
? I was using private member flags before, _position
and position()
. Maybe that’s really best, but I’ll let that idea perk for now.
So far, this doesn’t seem very different from the ‘linkages’ file, but it is a bit more clean.
I think we are stuck with the dictionary style of providing arguments to our class constructors. It’s tedious, but much less prone to errors than just listing a bunch of random values, plus it offers the possibility of optional arguments like leadDegrees
. I do have a tendency to write ()
instead of {}
, but the compiler quickly diagnoses that.
Luau supposedly has some type checking and I would like to be able to try it, but so far I’ve not figured out how to turn it on. I’ll put that in my backlog.
I’m feeling a small desire to have a Point object that is different from vector. I’m not sure what it would do for me. Possibly some additional methods on vector? But that’s problematical, because in the long term, vector will be provided by SLua and we won’t be using my hand-rolled one as we are now.
The method movingEnd
probably needs renaming at least, if not something more general. I was toying with the idea of dynamic names and functions that get plugged into the various classes. That may still be a good idea but I’m not ready to settle on it.
We need a way of specifying the root, and getting back the root position and object rotation.
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.