sokol-luajit/lua/opensteer/os-pathway.lua
2025-10-17 09:43:15 +10:30

237 lines
9.2 KiB
Lua

local veclib = require("opensteer.os-vec")
local osmath, osvec, Vec3 = veclib.osmath, veclib.osvec, veclib.vec3
local SphericalObstacle = require("opensteer.os-obstacle").SphericalObstacle
-- // ----------------------------------------------------------------------------
-- // PolylinePathway: a simple implementation of the Pathway protocol. The path
-- // is a "polyline" a series of line segments between specified points. A
-- // radius defines a volume for the path which is the union of a sphere at each
-- // point and a cylinder along each segment.
local pathway = {
PolylinePathway = function()
local self = {}
-- // xxx shouldn't these 5 just be local variables?
-- // xxx or are they used to pass secret messages between calls?
-- // xxx seems like a bad design
self.segmentLength = 0.0
self.segmentProjection = 0.0
self.localspace = Vec3()
self.chosen = Vec3()
self.segmentNormal = Vec3()
self.lengths = {}
self.normals = {}
self.totalPathLength = 0.0
-- // is the given point inside the path tube?
self.isInsidePath = function(point)
local outside
local tangent = Vec3()
local res = self.mapPointToPath(point, tangent, outside)
return res.outside < 0.0
end
-- // how far outside path tube is the given point? (negative is inside)
self.howFarOutsidePath = function(point)
local outside
local tangent = Vec3()
local res = self.mapPointToPath(point, tangent, outside)
return res.outside
end
self.pointCount = 0
self.points = {}
self.radius = 0.0
self.cyclic = false
-- // utility for constructors in derived classes
self.initialize = function( _pointCount, _points, _radius, _cyclic)
-- // set data members, allocate arrays
self.radius = _radius
self.cyclic = _cyclic
self.pointCount = _pointCount
self.totalPathLength = 0.0
if (self.cyclic) then self.pointCount = self.pointCount + 1 end
self.lengths = {}
self.points = {}
self.normals = {}
-- // loop over all points
for i = 1, self.pointCount do
-- // copy in point locations, closing cycle when appropriate
local closeCycle = self.cyclic and (i == self.pointCount)
local j = i
if closeCycle then j = 1 end
self.points[i] = _points[j]
-- // for the end of each segment
if (i > 1) then
-- // compute the segment length
self.normals[i] = self.points[i].sub( self.points[i-1] )
self.lengths[i] = self.normals[i].length()
-- // find the normalized vector parallel to the segment
self.normals[i] = self.normals[i].mult(1.0 / self.lengths[i])
-- // keep running total of segment lengths
self.totalPathLength = self.totalPathLength + self.lengths[i]
end
end
end
-- // Given an arbitrary point ("A"), returns the nearest point ("P") on
-- // this path. Also returns, via output arguments, the path tangent at
-- // P and a measure of how far A is outside the Pathway's "tube". Note
-- // that a negative distance indicates A is inside the Pathway.
self.mapPointToPath = function( point, tangent, outside)
local d
local minDistance = math.huge
local onPath = Vec3()
-- // loop over all segments, find the one nearest to the given point
for i = 2, self.pointCount do
self.segmentLength = self.lengths[i]
self.segmentNormal = self.normals[i]
d = self.pointToSegmentDistance(point, self.points[i-1], self.points[i])
if (d < minDistance) then
minDistance = d
onPath = self.chosen
tangent = self.segmentNormal
end
end
-- // measure how far original point is outside the Pathway's "tube"
outside = osvec.Vec3_distance(onPath, point) - self.radius
local res = { onPath=onPath, tangent=tangent, outside=outside }
-- // return point on path
return res
end
-- // given an arbitrary point, convert it to a distance along the path
self.mapPointToPathDistance = function(point)
local d
local minDistance = math.huge
local segmentLengthTotal = 0.0
local pathDistance = 0.0
for i = 2, self.pointCount do
self.segmentLength = self.lengths[i]
self.segmentNormal = self.normals[i]
d = self.pointToSegmentDistance(point, self.points[i-1], self.points[i])
if (d < minDistance) then
minDistance = d
pathDistance = segmentLengthTotal + self.segmentProjection
end
segmentLengthTotal = segmentLengthTotal + self.segmentLength
end
-- // return distance along path of onPath point
return pathDistance
end
-- // given a distance along the path, convert it to a point on the path
self.mapPathDistanceToPoint = function(pathDistance)
-- // clip or wrap given path distance according to cyclic flag
local remaining = pathDistance
if (self.cyclic) then
remaining = (pathDistance % self.totalPathLength)
else
if (pathDistance < 0.0) then return self.points[1] end
if (pathDistance >= self.totalPathLength) then return self.points[self.pointCount] end
end
-- // step through segments, subtracting off segment lengths until
-- // locating the segment that contains the original pathDistance.
-- // Interpolate along that segment to find 3d point value to return.
local result = Vec3()
for i = 2, self.pointCount do
self.segmentLength = self.lengths[i]
if (self.segmentLength < remaining) then
remaining = remaining - self.segmentLength
else
local ratio = remaining / self.segmentLength
result = osmath.interpolateV(ratio, self.points[i-1], self.points[i])
break
end
end
return result
end
-- // utility methods
-- // compute minimum distance from a point to a line segment
self.pointToSegmentDistance = function( point, ep0, ep1)
-- // convert the test point to be "local" to ep0
self.localspace = point.sub( ep0 )
-- // find the projection of "local" onto "segmentNormal"
self.segmentProjection = self.segmentNormal.dot(self.localspace)
-- // handle boundary cases: when projection is not on segment, the
-- // nearest point is one of the endpoints of the segment
if (self.segmentProjection < 0.0) then
self.chosen = ep0
self.segmentProjection = 0
return osvec.Vec3_distance(point, ep0)
end
if (self.segmentProjection > self.segmentLength) then
self.chosen = ep1
self.segmentProjection = self.segmentLength
return osvec.Vec3_distance(point, ep1)
end
-- // otherwise nearest point is projection point on segment
self.chosen = self.segmentNormal.mult( self.segmentProjection)
self.chosen = self.chosen.add( ep0 )
return osvec.Vec3_distance (point, self.chosen)
end
-- // assessor for total path length
self.getTotalPathLength = function() return self.totalPathLength end
return self
end,
}
-- // construct a PolylinePathway given the number of points (vertices),
-- // an array of points, and a path radius.
pathway.PolylinePathway1 = function(_pointCount, _points, _radius, _cyclic)
local pw = pathway.PolylinePathway()
pw.initialize (_pointCount, _points, _radius, _cyclic)
return pw
end
pathway.BuildPathway = function(data, scale)
data.pathPoints = {}
for k, pt in ipairs(data.points) do
data.pathPoints[k] = osvec.Vec3Set(pt[1], pt[2], pt[3])
end
data.obstacles = {}
for k, objs in ipairs(data.staticObjects) do
data.obstacles[k] = SphericalObstacle()
local vpos = go.get_world_position(objs.url)
data.obstacles[k].center = osvec.Vec3Set(vpos.x / scale, vpos.y / scale, vpos.z / scale)
data.obstacles[k].radius = objs.radius
end
data.pathway = pathway.PolylinePathway1(data.pathPointCount, data.pathPoints, data.pathRadius, false)
data.pathway.endpoint0 = data.pathway.points[1]
data.pathway.endpoint1 = data.pathway.points[data.pathPointCount]
return data
end
return pathway