Interpolator isn’t doing it for me. Let’s back that out and try something else. We’re here to learn.
Even though I have written a lot of Lua over the past few years, I would not claim to be a serious expert, neither in the secret crevices of metatables and such, nor even in the best ways to write code and create the data structures and values we need. So I am consciously taking this time between the SLua Alpha coming available and the day when we see it on the main grid as time to learn.
In that light, the Interpolator object was interesting but I don’t feel that it is carrying its weight. The creation of it was as least as awkward as the code for creating the original list in L_Bezier, and using it was really no simpler, replacing one line with a very similar line.
So I’ll keep the object and its tests, but I’ll back it out of the L_Bezier. Here it is, back in the prior form:
L_Bezier = class()
function L_Bezier:init(beziers)
self.length = 0
self._lengths = {}
self._points = {}
self.ready = false
for _, b in beziers do
self:_add(b.p0)
self:_add(b.p1)
self:_add(b.p2)
-- self:_add(b.p3)
end
self:_add(beziers[#beziers].p3)
end
function L_Bezier:_add(point)
local len
local n = #self._points
if n == 0 then
len = 0
else
len = point:dist(self._points[n])
end
self.length += len
table.insert(self._lengths, len)
table.insert(self._points, point)
end
function L_Bezier:find_distance(d)
local remaining_distance = d
for i, len in self._lengths do
if remaining_distance < len then
return i, remaining_distance/len
end
remaining_distance -= len
end
return nil
end
function L_Bezier:point_at_distance(d, debug)
local idx, frac = self:find_distance(d)
local p0 = self._points[idx-1]
local p1 = self._points[idx]
return p0*(1 - frac) + p1*frac
end
This morning I want to pay attention to the way we create the lengths and points arrays. In particular, I wonder whether things might go better if we created the points first and then derived the lengths from them, instead of doing it as part of the same operation.
And I think I’ll try expressing my intention a bit more clearly along the way.
L_Bezier = class()
function L_Bezier:init(beziers)
self._points = self:create_points(beziers)
self._lengths, length = self:create_lengths(self._points)
end
Given some Beziers, we’ll create the list of points from them, then we’ll create the list of lengths and the total length, from that list.
It could happen. If it does happen, the existing tests will run.
function L_Bezier:create_points(beziers)
local points = {}
local p3
for _, bezier in beziers do
table.insert(points, bezier.p0)
table.insert(points, bezier.p1)
table.insert(points, bezier.p2)
p3 = bezier.p3
end
table.insert(points, p3)
return points
end
At this moment I realize something: I think this code is probably right, but I can’t really test it until I write the other method as well. There was a smaller step to be made, even if I don’t see what it is right now.
Oh, I know. Let’s create this method but not use it, then we can test it separately. I’ll put the init
back as it was. I do that by just pasting it under the new one, so the old one overrides the new for now, and I can remove it again in a few moments, when all this works, if my plan comes to fruition.
Now I can test, very simply:
function Tests:test_l_bezier_create_points()
local b = self:sample_bezier()
local lb = b:linearized(3)
local old_points = lb._points
local new_points = lb:create_points(b:partition(3))
self:assert_equals(new_points, old_points)
end
That passes. My confidence increases a lot: that was worth doing. It also tells me how I might test the next bit, so let’s write that test as well.
function Tests:test_l_bezier_create_lengths()
local b = self:sample_bezier()
local lb = b:linearized(3)
local new_points = lb:create_points(b:partition(3))
local new_lengths = lb:create_lengths(new_points)
self:assert_equals(new_lengths, lb._lengths)
end
I think that’ll do. Let’s code the method.
function L_Bezier:create_lengths(points)
local lengths = {}
local total_length = 0
local previous = points[1]
for _, pt in points do
local len = previous:dist(pt)
total_length += len
table.insert(lengths, len)
previous = pt
end
return lengths, total_length
end
Tests pass. Remove the old init
, test again. Two tests fail. Defect is in the new init
. Should be this:
L_Bezier = class()
function L_Bezier:init(beziers)
self._points = self:create_points(beziers)
self._lengths, self.length = self:create_lengths(self._points)
end
I had left the self.
off the length
assignment. With it in place, all the tests pass.
OK, that’s pleasing. Let’s review what we’ve done. We’ve certainly added a method, and a loop over the points list that we didn’t have before. So this code is probably a tiny bit slower than the original. Is it more clear?
Here’s the original code from before, just the initialization part:
L_Bezier = class()
function L_Bezier:init(beziers)
self.length = 0
self._lengths = {}
self._points = {}
self.ready = false
for _, b in beziers do
self:_add(b.p0)
self:_add(b.p1)
self:_add(b.p2)
-- self:_add(b.p3)
end
self:_add(beziers[#beziers].p3)
end
function L_Bezier:_add(point)
local len
local n = #self._points
if n == 0 then
len = 0
else
len = point:dist(self._points[n])
end
self.length += len
table.insert(self._lengths, len)
table.insert(self._points, point)
end
Here’s the new version:
L_Bezier = class()
function L_Bezier:init(beziers)
self._points = self:create_points(beziers)
self._lengths, self.length = self:create_lengths(self._points)
end
function L_Bezier:create_points(beziers)
local points = {}
local p3
for _, bezier in beziers do
table.insert(points, bezier.p0)
table.insert(points, bezier.p1)
table.insert(points, bezier.p2)
p3 = bezier.p3
end
table.insert(points, p3)
return points
end
function L_Bezier:create_lengths(points)
local lengths = {}
local total_length = 0
local previous = points[1]
for _, pt in points do
local len = previous:dist(pt)
total_length += len
table.insert(lengths, len)
previous = pt
end
return lengths, total_length
end
The old version is 28 lines, the new is 32.
The create_points
is a bit tricky with that local p3. It could be like this:
function L_Bezier:create_points(beziers)
local points = {}
for _, bezier in beziers do
table.insert(points, bezier.p0)
table.insert(points, bezier.p1)
table.insert(points, bezier.p2)
end
local final_point = beziers[#beziers].p3
table.insert(points, final_point)
return points
end
Maybe that’s better. We’ll let that ride. Saved a line of code. We could inline the final_point
local but I think the name helps understanding and is worth keeping.
I think doing the creation of the points list and lengths list in two separate methods makes it more clear what is going on. It’s only a line or two longer and it includes no weird if statements such as we had in the old add
method. I think this is a keeper.
As I mentioned up top, I’m trying to learn good ways of doing things in SLua / Luau / Lua. I’m working to build up my intuition about how to program things, to build up my bag of tricks, as Kent Beck used to say. the more practice I get, the better my work will be when it comes time to work.
Fortunately, I love practicing.
Next time, we might think about our developer tool set. It’s not serving us as well as it might.
Until then, I hope you find only safe paths!