JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Good vs Good Enough

May 25, 2025 • [designluaobjectstesting]


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.

Good vs Good Enough

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.

It Gets Worse

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!