JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Moving Toward Moving

May 26, 2025 • [designluamoversobjectstesting]


I am feeling impatient. This tells me that I need to be extra careful today, but I really want to move a prim along a path.

I’m doing all this work toward tiny objects to develop a simple smooth design for our SLua generation of Valkyrie Transport products. We’re pretty close. I’m pleased with how the tests are going. But there is nothing like seeing an object move to satisfy one’s desire for progress.

I think there is just one more object that is needed. Maybe two. I feel that I could just bang them out and give a prim a ride. But no, the point is to practice and learn what good, simple, useful objects are like in this domain. Since I am feeling impatient, I’ll try to consciously slow down. There is no rush, Janet, there is no rush.

Multipath

Not Multipass, although that would be nice too. Multipath. A Multipath is an object, yet to be invented, that holds multiple paths. Thus the clever name. The paths it holds will be things like the current D_Bezier object, which is a particular kind of path.

The Multipath will maintain a list of length-and path information, very much like the waypoint information that we use in the D-Bezier. This time, I think, we’ll build it inline in the Multipath, and perhaps just use a simple dictionary rather than a full-on class. I’m not sure Waypoint being an actual class is bearing enough weight, so we’ll try something simpler as part of learning what we want.

I’m assuming that the Multipath will have many paths in it, so it will be built to start empty and allow us to add paths until done.

Let’s get started with a test.

function Tests:test_initial_multipath()
    mp = Multipath()
    self:assert_equals(mp.length, 0)
end

This is just a bit more than we need as an initial test. Just creating one would be the minimal test, but I am sure that the Multipath needs to know its current length, so I added in that assertion. Possible evidence of impatience. If I had any horses I would hold them.

Multipath = class()
function Multipath:init()
    self.length = 0
end

I think that will pass. It does. Excellent. I think we’ll move toward adding paths to it. Rather than gin up a D_Bezier containing a Bezier and all that, let’s use some dummy objects. I think that the Multipath’s job is just to keep track of some objects with lengths and, given a desired distance, return the object whose length encompasses that distance.

Hmm
This brings a thought to mind that I hadn’t considered. In principle, as our vehicle moves, incrementing its distance bit by bit, it could ask the Multipath for the relevant path on every move. If the Multipath were to search for that path every time, it would be slow. But I’d much prefer that we do refer to the Multipath every time rather than deal with caching values elsewhere. So probably we should cache the current path inside. Noted, we’ll write a test for that.

For now, let’s just write a test that adds a couple of fake paths with lengths and see if we can fetch them back.

function Tests:test_multipath_add_and_find()
    mp = Multipath()
    p1 = {length=2, path="p1"}
    p2 = {length=2, path="p2"}
    mp:add(p1)
    mp:add(p2)
    self:assert_equals(mp:find(1), p1)
    self:assert_equals(mp:find(3), p2)
end

Two paths, both length two. If we look for distance 1 we should get the first one, and at distance 3 the second one.

Hmm Again
I’m learning what this object is as I use tests to design it. I’m now recognizing that we’ll have to be more clear about what the paths know. In particular, somewhere we have to keep track of the fact that from the viewpoint of our fake path p2, its distance goes from 0 to its length. So when we actually put it into use, we need to adjust the requested distance accordingly.

Confusing? We’ll try to make it less so. For now, carry on.

Multipath = class()
function Multipath:init()
    self.paths = {}
    self.length = 0
end

function Multipath:add(path)
    local entry = {
        min_d=self.length, 
        max_d = self.length + path.length,
        path=path}
    table.insert(self.paths, entry)
    self.length = self.length + path.length
end

function Multipath:find(distance)
    for i,p in ipairs(self.paths) do
        if p.min_d <= distance and distance <= p.max_d then
            return p.path
        end
    end
    return nil
end

With the above code in place, the tests all pass. All 55 of them: remember that refactoring test that compares two ways of building up the Waypoints. We should pick a way and ignore or remove that test. Removing is the right thing to do, but it’s a bit more scary without Git backing me up here.

Idea
While working on this I had an idea about the way we might adjust a path, which thinks of itself as spanning distance 0 to length, when we know it is covering d to d+length for some distance d. One possibility is to do the adjustment up at the top, but we would much prefer that the top level code just keeps increasing distance without limit.

My very tentative idea is that we could have a sort of “distance-adjusting path” object that holds a path that thinks of itself as going from 0 to length but the distance-adjusting path knows that we think our path is at distance d and upwards, and the distance-adjusting path does the math.

Too weird? Perhaps, but if we can better serve our user code, which gets complicated for other reasons, it could be worth figuring out. I’ll add it to the list of ideas. Ideas are good at this stage. Perhaps at any stage.

Let’s assess what we have so far in our Multipath.

It knows some paths and it knows its total length. Currently if you ask for something beyond that length, it returns nil. Our needs are more complicated than that. If the path in the Multipath is closed, then when we come in with a distance of length + 3, we want it to act like 3. And similarly for negative distances, because sometimes the evil people who make our vehicles want them to be able to back up, and that tends to lead to negative distances.

Let’s write a test for values outside 0-length, specifying what we want and testing that we get it.

function Test:test_multipath_out_of_range()
    mp = Multipath()
    p1 = {length=5, path="p1"}
    p2 = {length=5, path="p2"}
    mp:add(p1)
    mp:add(p2)
    self:assert_equals(mp:find(11), p1)
    self:assert_equals(mp:find(16), p2)
    self:assert_equals(mp:find(-3), p2)
    self:assert_equals(mp:find(-7), p1)
end

function Multipath:find(distance)
    local d = distance % self.length
    for i,p in ipairs(self.paths) do
        if p.min_d <= d and d <= p.max_d then
            return p.path
        end
    end
    return nil
end

I just adjusted the distance by modding it with length. I am surprised that that worked, because I think in LSL mod can return a negative number, but it appears that it cannot in Lua.

I don’t think we can get the nil return unless the object is ill-formed somehow. I’ll make a note to look into that.

OK Where Are We?

I think the Multipath can find the right path given a distance. However, if we are attempting to move a prim according to a series of paths in a Multipath, we have to deal with the distance issue. If our Multipath has two segments of length 5, and we want distance 6, the Multipath will correctly return the second segment, but we won’t know to subtract the preceding length from the desired distance.

Time for some more broad thinking.

Here’s what I plan for my first motion test Let’s plan what our first actual motion test should look like.

There is an object, probably a Multipath, set up to contain enough Bezier instances to make for an interesting path to fly.

Our motion code should set its distance to zero, and then increment distance by small amounts. It thinks distance is in meters and in our small test we’ll only be moving maybe 4 our 5 meters away from where we start, so the step size, ten times per second, will be small, maybe 0.1 meters, giving a speed of one meter pr second.

I’d like the action in the timer event to be just about this simple:

function timer()
    vehicle:move(speed)
end

function vehicle:move(step)
    self.distance += step
    self:move_to(self.distance)
end

function vehicle:move_to(distance)
    local position = self.paths:at(distance)
end

I’m hand-waving here, because I’m not really sure about the objects at this level but inside the paths object, which I think is Multipath, something like this:

function Multipath:at(distance)
    local entry = self:find_entry(distance)
    local d = distance - entry.min_d
    return entry.path:at(d)
end

We find the entry (which is not what our test currently does), adjust the distance, and ask the path we got for its value at d. If it’s a D-Bezier, which I imagine it probably, its at method converts distance to t and asks its contained Bezier for its value at that t.

Bounce bounce bounce, a couple of lines of code at each level and we have what we want.

I’m tired and want a break and some caffeine. Let’s make find_entry work in Multipath and then do a quick summary and have that break.

We can just use find_entry in find and if the tests still run we know it works: we don’t need a new test. This is, in essence an Extract Method refactoring.

function Multipath:find_entry(distance)
    local d = distance % self.length
    for i,p in ipairs(self.paths) do
        if p.min_d <= d and d <= p.max_d then
            return p
        end
    end
    return self.paths[0]
end

function Multipath:find(distance)
    local p = self:find_entry(distance)
    return p.path
end

As Of Now …

I am not sure that our feet quite reach the ground with the objects we have so far, but we are close. We’ll set up a structure like this

Multipath
  D_Bezier 1
    Bezier 1
  D_Bezier 2
    Bezier_2
  ...

Then, to fly the Multipath, we will just set distance to zero and start incrementing it, asking the Multipath to give us the position at some distance. It will find its entry, ask that entry for the position at the (adjusted) distance, and we’ll move.

It might actually work as advertised right now: I’m not sure.

Initial code moving things is fragile and needs to be set up carefully so that the object doesn’t fly away never to be seen again. So I’d like to be quite fresh when I do the first motion experiment, and even then I’ll probably dump a lot of text to print before I let the thing actually move itself.

But overall, I am feeling good about what’s going on here. Perhaps you can begin to see how easy it will be to use these objects, even if you’re not quite clear yet on precisely how they work. Mind you, I’m not totally clear on that myself: we are learning as we go.

The idea is to go in small steps, making small objects and small functions, testing as we go so that when we do assemble things we are as confident as we can be that they all work as intended.

This isn’t a perfect scheme: I do still make mistakes. But most of them I find with my tests, and they generally show up right after I write the bug, so they are quickly found and fixed.

Until next time, safe paths!