Working toward a polyline creation and use scheme that seems nearly good. We’re learning here.
I want to get a sense of good ways to process our polyline structure: to my recollection, I’ve never done a mover that processes variable-length segments. So I want to get a sense of how that structure “wants” to be processed. Understanding that may lead to a different representation of the information, and a different way of creating it. We’ll tend to create it once and then use it many times, so we want to lean toward making the use easy even if the creation is a bit more involved.
When last we were here, I think we had created the partition of Beziers, a collection of 2^N beziers that trace the same path as an original single one. We do that because the control points of the partition can serve as points on a straight-line breakdown of the path, from one control point to the next and next and so on.
Let’s imagine some data structures and see what it takes to fetch points using them. We might do one, we might do more than one.
Along the way, we’ll discover some names for what we’re doing. Right now, I’m not sure. I’m just kind of sketching to get a notion of the final picture.
I’ll put some code in a test, though I am not sure just what I’ll test. Tests make a good place to keep little patches of code that we want to try out.
I start with this:
function Tests:test_polyline()
local b = self:sample_bezier()
local partition = b:partition_points(3)
local n = #partition
local path = {}
for i, point in partition do
table.insert(path, point)
if i < n then
local q = partition[i+1]
table.insert(path, point:dist(q))
end
end
local out = ""
for _, thing in path do
out = out .. tostring(thing) .. ', '
end
print(out)
end
I finally got around to extracting my standard Bezier creation into a method on Tests:
function Tests:sample_bezier()
local p0 = vector(400, 400, 0)
local p1 = vector(500, 500, 0)
local p2 = vector(600, 500, 0)
local p3 = vector(700, 400, 0)
return Bezier(p0, p1, p2, p3)
end
If I were a good person I would go back and change all the tests to use it. Perhaps later.
Along the way there I discovered that I don’t really want the set of Beziers that partition the original, so I asked the original Bezier for the partition_points
and then wrote the code to insert my lengths. The print is too complex to analyze but looks ok. I try partition(0) and get this:
table: 0x000000015b8bde20, 141.4213562373095,
table: 0x000000015b8bddf0, 100,
table: 0x000000015b8bddc0, 141.4213562373095, table: 0x000000015b8bdd90,
So that does look decent. I notice that the 141.42 thing is 100 times the square root of two, which is certainly interesting. Since p0 is (400,400, 0) and p1 is (500, 500, 0), the distance between them is surely 100*sqrt(2). My confidence increases even further.
I’m really here to see what it is like to process that structure, and I still don’t know that. I think I want a little class to help me out. I’ll call it … L_Bezier, L for linearized, and I’ll create it in Bezier. Change the test a bit.
function Tests:test_polyline()
local b = self:sample_bezier()
local l_bezier = b:linearized(0)
local path = l_bezier:get_path()
local out = ""
for _, thing in path do
out = out .. tostring(thing) .. ', '
end
print(out)
end
I’m just feeling my way here, pushing bricks around until I get something with a shape that feels right. I need linearized
.
function Bezier:linearized(n)
local beziers = self:partition(n)
local l_bezier = L_Bezier()
for _, b in beziers do
l_bezier:add(b.p0)
l_bezier:add(b.p1)
l_bezier:add(b.p2)
l_bezier:add(b.p3)
end
return l_bezier
end
I’m going to leave it up to L_Bezier to insert the lengths. I have an idea for that. I was going to write a test directly for L_Bezier but somehow I literally forgot and just implemented the class:
L_Bezier = class()
function L_Bezier:init()
self.path = {}
self.ready = false
end
function L_Bezier:add(point)
local path = self.path
if self.ready then
table.insert(path, path[#path]:dist(point))
end
table.insert(path, point)
self.ready = true
end
function L_Bezier:get_path()
return self.path
end
The trick is that we add a length ahead of each input point except the first. Nearly good.
However, now Bezier knows way too much about how to create an L_Bezier. Let’s move that capability to L_Bezier, which will have the useful side effect that we never ch
function Bezier:linearized(n)
local beziers = self:partition(n)
return L_Bezier(beziers)
end
L_Bezier = class()
function L_Bezier:init(beziers)
self.path = {}
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
end
function L_Bezier:_add(point)
local path = self.path
if self.ready then
table.insert(path, path[#path]:dist(point))
end
table.insert(path, point)
self.ready = true
end
function L_Bezier:get_path()
return self.path
end
My test still runs. I think I’ll enhance it a bit and remove the print.
function Tests:test_polyline()
local b = self:sample_bezier()
local l_bezier = b:linearized(0)
local path = l_bezier:get_path()
self:assert_equals(path[1], b.p0)
self:assert_nearly_equal(path[2], 141.42, .01)
self:assert_equals(path[#path], b.p3)
end
Well, it is time to break, and as often happens, I didn’t quite do what I thought I might, but instead I did some good things leading up to that thing. Fortunately, I didn’t say right up front what I thought would happen.
What did happen was that we wrote some open code in a test, creating an alternating array of point, length, point, length, point. Evolving that, we wrote a useful helper method in the tests, then added capability to Bezier, used that capability, realized that the capability belonged to Bezier, moved it there, then finally saw a new object, L_Bezier, LinearizedBezier, and divided up the responsibility.
That division is simple, but I want to underline it.
Doing it that way puts responsibility in the object that knows best what it wants, instead of having the Bezier make assumptions about what the L_Bezier wants or vice versa.
Is this division of responsibility perfect? Probably not: we rarely achieve perfection. But it’s better than the three or four ways we had it before we got here.
Do better. After a while, we’re doing pretty well.
Safe paths!