JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Refinement+=1

Aug 4, 2025 • [luatesting]


More by way of exercise than purpose, I propose to continue “refining” the production of split bezier points.

Sooner or later, I plan to plug the splits into my Pathfinder object, or a variant thereof, with an eye to when we’ll actually use the thing. If we ever do. But at least to start, I want to see if I can come up with something better than what we have, which is this:

BezierSplitter = class()
function BezierSplitter:init(array)
    self.array = array
end

function BezierSplitter:polyline(numberOfSplits)
    local result = {}
    local splits = self:split(numberOfSplits)
    for i = 1, #splits, 4 do
        table.insert(result, splits[i])
        table.insert(result, splits[i+1])
        table.insert(result, splits[i+2])
    end
    table.insert(result, splits[#splits])
    return result
end

function BezierSplitter:split(numberOfSplits)
    local array = self.array
    for i = 1, numberOfSplits do
        array = self:split_by_fours(array)
    end
    return array
end

function BezierSplitter:split_by_fours(array)
    local result = {}
    for i = 1, #array, 4 do
        local a = array[i]
        local b = array[i+1]
        local c = array[i+2]
        local d = array[i+3]
        local e = (a+b)/2
        local f = (b+c)/2
        local g = (c+d)/2
        local h = (e+f)/2
        local j = (f+g)/2
        local k = (h+j)/2
        for _, p in {a,e,h,k, k,j,g,d} do
            table.insert(result, p)
        end
    end
    return result
end

The scheme we have here seems to me to be questionable in a few ways:

  1. It spends a lot of time copying values from one list to another;
  2. It is iterative and possibly could be recursive to some possible advantage;
  3. It has a final pass where it removes duplicate values from the result, returning a true polyline.

After further messing about this morning, I really don’t have anything that both works and is simpler. I have simpler and faster code that doesn’t work and never will, but I have little use for code that doesn’t work.

The larger idea is to use this simpler class, BezierSplitter, in place of Bezier, to produce the information we need in our motion code.

I think the class we have in mind is the PolylinePath class, which accepts a polyline, calculates the lengths of the segments, and returns interpolated points.

I think we just have to replicate the tests we have for that class using the BezierSplitter instead of Bezier.

I do that and the sky falls. The PolylinePath object isn’t prepared for my shenanigans. I quickly fix up the first failure, setting the others to ignore. Now I’ll tick through one at a time. The first one just needs an old-style bezier to compare values with, readily fixed up.

The remaining two are similar, checking the internal results in PolylinePath:

_:test('test_polyline_path_create_points', function()
    local pp = PolylinePath(splitter:polyline(3))
    local old_points = pp._points
    local new_points = pp:_create_polyline(splitter:polyline(3))
    _:expect(new_points).is(old_points)
end)

_:ignore('test_polyline_path_create_lengths', function()
    local pp = PolylinePath(splitter:polyline(3))
    local new_points = pp:_create_polyline(splitter:polyline(3))
    local new_lengths = pp:_create_lengths(new_points)
    _:expect(new_lengths, p).is(pp._lengths)
end)

I do think these need checking, but this isn’t a very good way of doing it. The original tests aren’t much more enlightening:

_:test('test_polyline_path_create_points', function()
    local b = sample_bezier()
    local pp = PolylinePath:from_beziers(b:partition(3))
    local old_points = pp._points
    local new_points = pp:_create_polyline(b:partition(3))
    _:expect(new_points).is(old_points)
end)

_:test('test_polyline_path_create_lengths', function()
    local b = sample_bezier()
    local pp = PolylinePath:from_beziers(b:partition(3))
    local new_points = pp:_create_polyline(b:partition(3))
    local new_lengths = pp:_create_lengths(new_points)
    _:expect(new_lengths, p).is(pp._lengths)
end)

It seems like these are doing no more than checking that PolylinePath calls its own internal methods.

Let’s see if we can gin up an actual test for lengths and points. What does PolylinePath actually do?

PolylinePath = class()
-- contains variable-length linear segments

function PolylinePath:init(polyline)
   self._points = polyline
   self._lengths, self._length = self:_create_lengths(self._points)
end

function PolylinePath:_create_lengths(points)
    local lengths = {0}
    local total_length = 0
    for p0, p1 in self:do_by_pairs() do
        local len = p0:dist(p1)
        table.insert(lengths, len)
        total_length += len
    end
    return lengths, total_length
end

It saves the points as is, not much point checking that, and puts in _lengths the length of the segment from the preceding point to this one. I think we can create data to give us predictable answers.

_:test('test_polyline_path_create_lengths', function()
    local polyline = {
        vector(0,0,0),
        vector(1,0,0),
        vector(3,0,0),
        vector(3,33,0)
    }
    local pp = PolylinePath(polyline)
    _:expect(pp._lengths).is({0,1,2,33})
end)

Carefully constructed vector data lets me predict the lengths. And the test passes just fine. Since the other test just checks to see if we put the points in properly, we can remove it, since with the Splitter we don’t unpack Beziers to get the points, we pass them in and use them as is.

Break time. Check tests, commit code.

Summary

We can use the BezierSplitter class instead of Bezier, and save some code We can only do that if we have no need to compute actual Bezier values, which would entail changes to the SLRR-capable movers, so it remains to be seen whether we’ll want to do that.

With BezierSplitter and no Bezier, the PolylinePath will be both smaller and faster in use, as we can remove the unused creation methods that work from Beziers.

With these two classes as modified, we’ll have smaller code and faster execution with less garbage creation, which should be an overall win. Note that performance has not been measured to prove all that. But I’d bet a day’s pay on it.

My experience working in the bezier_dev file tells me that it is time to pull out the experiments that are not likely to go forward, and produce a smaller file with the good stuff in it. As it stands, the file is 1041 lines, a new record for me by far, and includes eight feature test suites, and seven classes, six of which are actual classes, while one is just a local class used in a test.

Classes are: Leaf (the test one), Bezier, BezierSplitter, FixedIntervalPath, PathFinder, Polyline, and PolylinePath. Of those, BezierSplitter and PolylinePath are possible candidates for live use, and possibly FixedIntervalPath, although I rather suspect it won’t be needed.

If I had to guess right now, I’d guess that we might adapt the BezierSplitter and PolylinePath only if we decide to convert the SLRR_capable movers to SLua and that even those would not be likely to be used in their present form.

Basically, all this work has been about developing ideas, experience, and expertise. It has been worth it for that. As for the actual code? Quite likely not useful as it stands, perhaps useful for mining ideas and techniques.

Odd? Perhaps, but it’s my mornings.

Safe paths!