JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Better Idea ...

Jun 16, 2025 • [designluamoverstesting]


After thinking about searching in my new L_Bezier, I think I’ll try a different storage scheme. Summary article, infinite painful detail left out.

Review

We have various kinds of paths that our vehicles follow, including

The mover code wants to think in terms of distance traveled vs time, meters per second kind of thing. That’s easy in the 0.5 meter case, take the distance you want, divide by 0.5, and there’s the subscript of the points you need, interpolate and all good.

But Bezier curves don’t understand distance, so we have to search back and forth to find the distance we want, resulting in evaluating the Bezier multiple times in every timer cycle. It would be nice to do something more efficient.

The current trial solution is the L_Bezier object, standing for “linearized bezier”. Simply, the L_Bezier represents a collection of points at varying distance from each other, making up a straight line segment path (a polyline) that approximates the original Bezier well.

We can ask an L_Bezier for point_at_distance(d), and it will produce the interpolated point along the polyline segment that contains distance d: just what we need.

In the preceding article, I implemented L_Bezier using a sort of fence-post data structure of alternating points and lengths:

p1--l1--p2---l2---p3-l3-p4----l4-----p5

I’ve found this alternating structure hard to build, and it is no easier to process for looking up the info we need. So the new plan is to store the lengths separately, and to start the length array with a 0 value, which I think will make searching easier.

The L_Bezier will now keep two arrays, one of the lengths of its segments, and the other the consecutive points of the polyline that makes it up.

_lengths   0 p1-p0 p2-p1 p3-p2
_points   p0   p1    p2    p3

So when we find the index of the length we desire, the far point is under the index and the near point is in the previous cell.

The L_Bezier class is pretty simple:

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
      end
      remaining_distance -= len
   end
   return nil
end

function L_Bezier:point_at_distance(d)
   local idx, remaining_distance = self:find_distance(d)
   local p0 = self._points[idx-1]
   local p1 = self._points[idx]
   local len = self._lengths[idx]
   local frac = remaining_distance/self._lengths[idx]
   result =  p0*(1 - frac) + p1*frac 
   return result
end

function L_Bezier:dump()
   s = ''
   for i,len in self._lengths do
      s = s .. tostring(len) .. ' '
   end
   print(s)
   s = ''
   for i, pt in self._points do
      s = s .. tostring(pt) .. ' '
   end
   print(s)
end

I have a few tests for it. I have seen them break so often that I am sure they suffice to show that it works as intended. Here they are:

function Tests:test_l_bezier()
   local b = self:sample_bezier()
   local l_bezier = b:linearized(0)
   local lengths = l_bezier._lengths
   local points = l_bezier._points
   self:assert_equals(lengths[1], 0)
   self:assert_nearly_equal(lengths[2], 141.42, .01)
   self:assert_equals(points[1], b.p0)
   self:assert_equals(points[4], b.p3)
end

function Tests:test_l_bezier_find()
   local b = self:sample_bezier()
   local lb = b:linearized(3)
   local first_len = lb._lengths[2]
   local index = lb:find_distance(first_len / 2)
   self:assert_equals(index, 2)

end

function Tests:test_l_bezier_interpolation()
   local b = self:sample_bezier()
   local lb = b:linearized(0)
   -- lb:dump()
   local sqrtby2 = 100*math.sqrt(2) / 2
   pt = lb:point_at_distance(lb.length/2)
   self:assert_nearly_equal(pt.x, 550, 0.01, "x")
   self:assert_nearly_equal(pt.y, 500, 0.01, "y")
end

function Tests:test_l_bezier_vs_bezier()
   local pt
   local bpt
   local b = self:sample_bezier()
   local lb = b:linearized(3)
   pt = lb:point_at_distance(0)
   bpt = b:at(0)
   local idx, d = lb:find_distance(0)
   self:assert_nearly_equal(pt.x, bpt.x, 0.01, "zero x")
   self:assert_nearly_equal(pt.y, bpt.y, 0.01, "zero y")
   local len = lb.length
   pt = lb:point_at_distance(len/2)
   bpt = b:at(1/2)
   self:assert_nearly_equal(pt.x, bpt.x, 0.01, "x")
   self:assert_nearly_equal(pt.y, bpt.y, 0.01, "y")
end

Effort

I had a terrible time yesterday, and nearly again today, and I’ve decided to erase all that painful detail from the article. Not a pretty sight, me thrashing like a fish on the line.

Results

I am now quite confident (0.95) that the linearized method on Bezier returns an L_Bezier instance that correctly represents the line segments of a series of small Beziers that add up to the original Bezier.

And I’m pretty confident (0.85+) that the point_at_distance of L_Bezier does the correct interpolation to find the point part-way along the correctly-chosen segment. In short, I think it works.

Lessons Learned

Weak Tests
The important tests of L_Bezier, the ones that I had trouble making work, basically check the calculated Bezier function, which as all know is a strange cubic expression that is essentially impossible to predict almost everywhere on the curve, and subject to rounding and similar differences almost everywhere.

The trouble I had was all in the point_at_distance method, the one that does the interpolation. If I had figured out a way to test that function directly, I might have seen right away all the ways it wasn’t working, and found the one that works much sooner.

That should be possible. I’ll make a note to try it as an exercise. The general rule is that tests should b very close to the tiny thing you’re interested in, rather than checking remote values that will be right if the tiny thing works.

Too Intricate?
I’ll review this code later. The idea seems quite simple, and yet the interpolation code was difficult to write and to get right. That makes me think there’s something better waiting to be found, something simpler, more clear, easier to write and understand.

I could be right; I could be wrong. I’ll try, and find out.

First Rule of Holes
When you find yourself in a hole, stop digging. I’d have done well to stop much sooner yesterday.

Today wasn’t so bad, though I did kind of debug around for a while. I did develop a handy testing trick that I’ll perhaps write up later.

Summary

I think the L_Bezier idea is a decent one, supporting a variable-length linear segmentation of a Bezier. I had great difficulty making it work, but it seems pretty simple and perhaps can be more so. We’ll see, in due time.

Further work will include the rotation info that we need, not just the position info. Should be straightforward, but rotations are the work of the devil, so there’s always something.

For now, we’ve got what we set out to get. All’s well that ends, innit?

Safe paths!