In which, I consider what I’m up to, and try a different approach, an iterator.
Last night, with nothing on the tube, well, big flat panel, that I wanted to watch, and tired of the somewhat inane dragon detective in the novel I was reading, I took a look at yesterday’s article and tried something different on my iPad.
I tried something different because I realized that I don’t much like yesterday’s solution to my waypointing task. It was easier to write than the previous version, but the two-loop solution doesn’t make it easier to understand.
My work is set back a bit because SL has wiped out my objects and scripts, but fortunately I have a copy of yesterday’s work still here in Sublime Text. Ready to go.
My current purpose with SLua is to learn enough to use it well, with a particular focus on how to build our Valkyrie Transport vehicles in SLua. Our existing code is many generations old, and in addition to the inherent difficulties of doing anything complicated in LSL, it has become a bit how can I best put this, messy. My intuition is that we can do much better with SLua than we ever could with LSL, and I want to learn how to do that.
When a developer is working under pressure or against a deadline, often they have to settle for code that is “good enough”. It does the job, and although it could be improved, the pressures of the project make it better—or seemingly better—to move on to other areas, writing more code that is “good enough”.
In my view, if we add up a large amount of “good enough”, we often get to “not very good”. Aside: this is the fundamental problem of LLM-based AI, by the way: it is inherently mediocre.
So now, while we’re just learning, I want to try many ways of doing things, and I want to get to code that I consider “good”, not just “good enough”. I believe that if I get more practice writing better code, my average code quality will improve. How could it not?
So last night I experimented with an iterator, which probably not entirely coincidentally was the topic for last week’s advanced SLua class from SuzannaLinn. I’ve written iterators before, in Python, and since I wrote not just one but two last night on the iPad, I have written iterators in Lua as well. So let’s change the refactored
version of our make_waypoints
method to use an iterator.
I start by erasing the contents of the method:
function D_Bezier:make_waypoints_refactored(intervals)
end
I had intended to start with some comments saying what the method does. I found it hard to explain. If I can’t explain clearly what I’m trying to accomplish, my chances of accomplishing it seem lower than one might like.
A Waypoint is an object with four members and two methods:
Waypoint = class()
function Waypoint:init(min_d, max_d, min_t, max_t)
self.min_d = min_d
self.max_d = max_d
self.min_t = min_t
self.max_t = max_t
self.length = max_d - min_d
self.delta_t = max_t - min_t
end
function Waypoint:contains(distance)
return self.min_d <= distance and distance <= self.max_d
end
function Waypoint:evaluate(distance)
local frac_d = (distance - self.min_d)/self.length
return self.min_t + self.delta_t*frac_d
end
The Waypoint promises that if you are looking for a distance d
along the curve that the Waypoint describes, and the distance you are looking for is between min_d
and max_d
, then the t you are looking for is between min_t
and max_t
, so that if you evaluate
that distance, you’ll get the t
you need to provide to the related Bezier (or whatever kind of t-parameterized curve it may be).
The make_waypoints
method chops up a Bezier (or any t-parameterized curve) into Waypoint. Each Waypoint covers one Nth of the zero to one limits of the t
parameter, and records the cumulative distances at each end of that Waypoint.
My larger objective just now is to come up with a number of tiny useful objects for doing what we need to do with our vehicles. The objects I’ve built over the past few days have been speculative. I think they’ll be useful but mostly I’m playing with small ideas without being certain how they’ll fit together.
As I was explaining things just above, it began to seem to me that these object may not be quite what we’ll want.
That’s OK, for now. We are making widgets out of tiny objects, to get a feeling for the process of making widgets, without a commitment to any particular widget assembly as yet.
Carry on, Janet, do your iterator.
OK, I’ll do it without the comment at the top that I still can’t write. After almost no trouble my comparison test passes with this code:
function D_Bezier:make_waypoints_refactored(number_of_waypoints)
function waypoints(num)
local step = 0
local len = 0
return function()
if step < num then
local t0 = step/num
local t1 = (step+1)/num
local p0 = self.bezier:at(t0)
local p1 = self.bezier:at(t1)
local span = vector.magnitude(p1 - p0)
local wp = Waypoint(len, len+span, t0, t1)
len = len + span
step = step + 1
return wp
end
end
end
wps = {}
for wp in waypoints(number_of_waypoints) do
table.insert(wps, wp)
end
return wps
end
Let’s see if we can refactor this a bit. I think I’d like the big waypoints function to be outside the main method.
function D_Bezier:make_waypoints_refactored(number_of_waypoints)
wps = {}
for wp in self:waypoint_iterator(number_of_waypoints) do
table.insert(wps, wp)
end
return wps
end
function D_Bezier:waypoint_iterator(num)
local step = 0
local len = 0
return function()
if step < num then
local t0 = step/num
local t1 = (step+1)/num
local p0 = self.bezier:at(t0)
local p1 = self.bezier:at(t1)
local span = vector.magnitude(p1 - p0)
local wp = Waypoint(len, len+span, t0, t1)
len = len + span
step = step + 1
return wp
end
end
end
Maybe inline the p0 and p1, like this:
function D_Bezier:waypoint_iterator(num)
local step = 0
local len = 0
return function()
if step < num then
local t0 = step/num
local t1 = (step+1)/num
local span = vector.magnitude(self.bezier:at(t1) - self.bezier:at(t0))
local wp = Waypoint(len, len+span, t0, t1)
len = len + span
step = step + 1
return wp
end
end
end
Would a guard clause be better?
function D_Bezier:waypoint_iterator(num)
local step = 0
local len = 0
return function()
if step >= num then return nil end
local t0 = step/num
local t1 = (step+1)/num
local span = vector.magnitude(self.bezier:at(t1) - self.bezier:at(t0))
local wp = Waypoint(len, len+span, t0, t1)
len = len + span
step = step + 1
return wp
end
end
I think so. How about a multiple-assignment for t0 and t1?
function D_Bezier:waypoint_iterator(num)
local step = 0
local len = 0
return function()
if step >= num then return nil end
local t0, t1 = step/num, (step+1)/num
local span = vector.magnitude(self.bezier:at(t1) - self.bezier:at(t0))
local wp = Waypoint(len, len+span, t0, t1)
len = len + span
step = step + 1
return wp
end
end
I have mixed feelings about the inlining of the bez calls, and I hate the vector.magnitude
that we are using. We could use ll.VecDist
I guess. I do the two bez calls in a multiple assignment and use ll.VecDist
to get this:
function D_Bezier:waypoint_iterator(num)
local step = 0
local len = 0
return function()
if step >= num then return nil end
local t0, t1 = step/num, (step+1)/num
local p0, p1 = self.bezier:at(t0), self.bezier:at(t1)
local span = ll.VecDist(p0, p1)
local wp = Waypoint(len, len+span, t0, t1)
len = len + span
step = step + 1
return wp
end
end
Let’s do one renaming to see if we like it:
function D_Bezier:waypoint_iterator(num)
local step = 0
local length_so_far = 0
return function()
if step >= num then return nil end
local t0, t1 = step/num, (step+1)/num
local p0, p1 = self.bezier:at(t0), self.bezier:at(t1)
local span = ll.VecDist(p0, p1)
local wp = Waypoint(length_so_far, length_so_far+span, t0, t1)
length_so_far = length_so_far + span
step = step + 1
return wp
end
end
OK. We’ll sit with this for a while. We have now, in the original:
function D_Bezier:make_waypoints(intervals)
-- for _, interval in {4, 8, 16, 32, 64, 128, 1024} do
-- print(interval, self.bezier:compute_length(interval))
-- end
local wps = {}
local min_d = 0
local min_t = 0
local min_pos = self.bezier:at(min_t)
for i = 1, intervals do
local max_t = i/intervals -- 1/8, 2/8
local max_pos = self.bezier:at(max_t)
local span = vector.magnitude(max_pos-min_pos)
local max_d = min_d + span
wp = Waypoint(min_d, max_d, min_t, max_t)
table.insert(wps, Waypoint(min_d, max_d, min_t, max_t))
-- print(`WP {min_d}, {max_d}, {min_t}, {max_t}`)
min_t = max_t
min_d = max_d
min_pos = max_pos
end
return wps
end
And with the iterator, so far:
function D_Bezier:make_waypoints_refactored(number_of_waypoints)
wps = {}
for wp in self:waypoint_iterator(number_of_waypoints) do
table.insert(wps, wp)
end
return wps
end
function D_Bezier:waypoint_iterator(num)
local step = 0
local length_so_far = 0
return function()
if step >= num then return nil end
local t0, t1 = step/num, (step+1)/num
local p0, p1 = self.bezier:at(t0), self.bezier:at(t1)
local span = ll.VecDist(p0, p1)
local wp = Waypoint(length_so_far, length_so_far+span, t0, t1)
length_so_far = length_so_far + span
step = step + 1
return wp
end
end
I think I like the new way better, but we already know that I often like something more than it deserves right after I do it. So, we’ll live with it for a while, but I think that at least in the new form a person (who understands the iterator concept) can see how the Waypoints are being formed more readily than in the original.
I could be wrong. It wouldn’t be the first time.
Until the next time I’m wrong, safe paths!