2025-04-16 21:38:42 +08:00

750 lines
23 KiB
Lua

local ____lualib = require("lualib_bundle")
local __TS__Class = ____lualib.__TS__Class
local __TS__New = ____lualib.__TS__New
local __TS__ArraySplice = ____lualib.__TS__ArraySplice
local ____exports = {}
local T_VECTORS, T_ARRAYS
--- Flattens the specified array of points onto a unit vector axis,
-- resulting in a one dimensional range of the minimum and
-- maximum value on that axis.
--
-- @param points The points to flatten.
-- @param normal The unit vector axis to flatten on.
-- @param result An array. After calling this function,
-- result[0] will be the minimum value,
-- result[1] will be the maximum value.
function ____exports.flattenPointsOn(points, normal, result)
local min = 2 ^ 1024
local max = -2 ^ 1024
local len = #points
do
local i = 0
while i < len do
local dot = points[i + 1]:dot(normal)
if dot < min then
min = dot
end
if dot > max then
max = dot
end
i = i + 1
end
end
result[1] = min
result[2] = max
end
--- Check whether two convex polygons are separated by the specified
-- axis (must be a unit vector).
--
-- @param aPos The position of the first polygon.
-- @param bPos The position of the second polygon.
-- @param aPoints The points in the first polygon.
-- @param bPoints The points in the second polygon.
-- @param axis The axis (unit sized) to test against. The points of both polygons
-- will be projected onto this axis.
-- @param response A Response object (optional) which will be populated
-- if the axis is not a separating axis.
-- @returns true if it is a separating axis, false otherwise. If false,
-- and a response is passed in, information about how much overlap and
-- the direction of the overlap will be populated.
function ____exports.isSeparatingAxis(aPos, bPos, aPoints, bPoints, axis, response)
local rangeA = table.remove(T_ARRAYS)
local rangeB = table.remove(T_ARRAYS)
local offsetV = table.remove(T_VECTORS):copy(bPos):sub(aPos)
local projectedOffset = offsetV:dot(axis)
____exports.flattenPointsOn(aPoints, axis, rangeA)
____exports.flattenPointsOn(bPoints, axis, rangeB)
rangeB[1] = rangeB[1] + projectedOffset
rangeB[2] = rangeB[2] + projectedOffset
if rangeA[1] > rangeB[2] or rangeB[1] > rangeA[2] then
T_VECTORS[#T_VECTORS + 1] = offsetV
T_ARRAYS[#T_ARRAYS + 1] = rangeA
T_ARRAYS[#T_ARRAYS + 1] = rangeB
return true
end
if response then
local overlap = 0
if rangeA[1] < rangeB[1] then
response.aInB = false
if rangeA[2] < rangeB[2] then
overlap = rangeA[2] - rangeB[1]
response.bInA = false
else
local option1 = rangeA[2] - rangeB[1]
local option2 = rangeB[2] - rangeA[1]
overlap = option1 < option2 and option1 or -option2
end
else
response.bInA = false
if rangeA[2] > rangeB[2] then
overlap = rangeA[1] - rangeB[2]
response.aInB = false
else
local option1 = rangeA[2] - rangeB[1]
local option2 = rangeB[2] - rangeA[1]
overlap = option1 < option2 and option1 or -option2
end
end
local absOverlap = math.abs(overlap)
if absOverlap < response.overlap then
response.overlap = absOverlap
response.overlapN:copy(axis)
if overlap < 0 then
response.overlapN:reverse()
end
end
end
T_VECTORS[#T_VECTORS + 1] = offsetV
T_ARRAYS[#T_ARRAYS + 1] = rangeA
T_ARRAYS[#T_ARRAYS + 1] = rangeB
return false
end
--- Checks whether polygons collide.
--
-- @param a The first polygon.
-- @param b The second polygon.
-- @param response Response object (optional) that will be populated if they interset.
-- @returns true if they intersect, false if they don't.
function ____exports.testPolygonPolygon(a, b, response)
local aPoints = a.calcPoints
local aLen = #aPoints
local bPoints = b.calcPoints
local bLen = #bPoints
local i
do
i = 0
while i < aLen do
if ____exports.isSeparatingAxis(
a.pos,
b.pos,
aPoints,
bPoints,
a.normals[i + 1],
response
) then
return false
end
i = i + 1
end
end
do
i = 0
while i < bLen do
if ____exports.isSeparatingAxis(
a.pos,
b.pos,
aPoints,
bPoints,
b.normals[i + 1],
response
) then
return false
end
i = i + 1
end
end
if response then
response.a = a
response.b = b
response.overlapV:copy(response.overlapN):scale(response.overlap)
end
return true
end
--- Represents a vector in two dimensions with `x` and `y` properties.
____exports.Vector = __TS__Class()
local Vector = ____exports.Vector
Vector.name = "Vector"
function Vector.prototype.____constructor(self, x, y)
if x == nil then
x = 0
end
if y == nil then
y = 0
end
self.x = x
self.y = y
end
function Vector.prototype.copy(self, other)
self.x = other.x
self.y = other.y
return self
end
function Vector.prototype.clone(self)
return __TS__New(____exports.Vector, self.x, self.y)
end
function Vector.prototype.perp(self)
local x = self.x
self.x = self.y
self.y = -x
return self
end
function Vector.prototype.rotate(self, radians)
local x = self.x
local y = self.y
self.x = x * math.cos(radians) - y * math.sin(radians)
self.y = x * math.sin(radians) + y * math.cos(radians)
return self
end
function Vector.prototype.reverse(self)
self.x = -self.x
self.y = -self.y
return self
end
function Vector.prototype.normalize(self)
local d = self:len()
if d > 0 then
self.x = self.x / d
self.y = self.y / d
end
return self
end
function Vector.prototype.add(self, other)
self.x = self.x + other.x
self.y = self.y + other.y
return self
end
function Vector.prototype.sub(self, other)
self.x = self.x - other.x
self.y = self.y - other.y
return self
end
function Vector.prototype.scale(self, x, y)
if y == nil then
y = x
end
self.x = self.x * x
self.y = self.y * y
return self
end
function Vector.prototype.project(self, other)
local amt = self:dot(other) / other:len2()
self.x = amt * other.x
self.y = amt * other.y
return self
end
function Vector.prototype.projectN(self, other)
local amt = self:dot(other)
self.x = amt * other.x
self.y = amt * other.y
return self
end
function Vector.prototype.reflect(self, axis)
local x = self.x
local y = self.y
self:project(axis):scale(2)
self.x = self.x - x
self.y = self.y - y
return self
end
function Vector.prototype.reflectN(self, axis)
local x = self.x
local y = self.y
self:projectN(axis):scale(2)
self.x = self.x - x
self.y = self.y - y
return self
end
function Vector.prototype.dot(self, other)
return self.x * other.x + self.y * other.y
end
function Vector.prototype.len2(self)
return self:dot(self)
end
function Vector.prototype.len(self)
return math.sqrt(self:len2())
end
--- Represents an axis-aligned box, with a width and height.
____exports.Box = __TS__Class()
local Box = ____exports.Box
Box.name = "Box"
function Box.prototype.____constructor(self, pos, w, h)
if pos == nil then
pos = __TS__New(____exports.Vector)
end
if w == nil then
w = 0
end
if h == nil then
h = 0
end
self.pos = pos
self.w = w
self.h = h
end
function Box.prototype.toPolygon(self)
local pos = self.pos
local w = self.w
local h = self.h
return __TS__New(
____exports.Polygon,
__TS__New(____exports.Vector, pos.x, pos.y),
{
__TS__New(____exports.Vector, 0, 0),
__TS__New(____exports.Vector, w, 0),
__TS__New(____exports.Vector, w, h),
__TS__New(____exports.Vector, 0, h)
}
)
end
--- Represents a circle with a position and a radius.
____exports.Circle = __TS__Class()
local Circle = ____exports.Circle
Circle.name = "Circle"
function Circle.prototype.____constructor(self, pos, r)
if pos == nil then
pos = __TS__New(____exports.Vector)
end
if r == nil then
r = 0
end
self.pos = pos
self.r = r
self.offset = __TS__New(____exports.Vector)
end
function Circle.prototype.getAABBAsBox(self)
local r = self.r
local corner = self.pos:clone():add(self.offset):sub(__TS__New(____exports.Vector, r, r))
return __TS__New(____exports.Box, corner, r * 2, r * 2)
end
function Circle.prototype.getAABB(self)
return self:getAABBAsBox():toPolygon()
end
function Circle.prototype.setOffset(self, offset)
self.offset = offset
return self
end
--- Represents a *convex* polygon with any number of points (specified in counter-clockwise order)
-- Note: Do _not_ manually change the `points`, `angle`, or `offset` properties. Use the
-- provided setters. Otherwise the calculated properties will not be updated correctly.
-- `pos` can be changed directly.
____exports.Polygon = __TS__Class()
local Polygon = ____exports.Polygon
Polygon.name = "Polygon"
function Polygon.prototype.____constructor(self, pos, points)
if pos == nil then
pos = __TS__New(____exports.Vector)
end
if points == nil then
points = __TS__New(Array)
end
self.pos = pos
self.points = {}
self.angle = 0
self.offset = __TS__New(____exports.Vector)
self.calcPoints = {}
self.edges = {}
self.normals = {}
self:setPoints(points)
end
function Polygon.prototype.setPoints(self, points)
local lengthChanged = not self.points or #self.points ~= #points
if lengthChanged then
local i
local ____temp_0 = {}
self.calcPoints = ____temp_0
local calcPoints = ____temp_0
local ____temp_1 = {}
self.edges = ____temp_1
local edges = ____temp_1
local ____temp_2 = {}
self.normals = ____temp_2
local normals = ____temp_2
do
i = 0
while i < #points do
do
local p1 = points[i + 1]
local p2 = i < #points - 1 and points[i + 1 + 1] or points[1]
if p1 ~= p2 and p1.x == p2.x and p1.y == p2.y then
__TS__ArraySplice(points, i, 1)
i = i - 1
goto __continue29
end
calcPoints[#calcPoints + 1] = __TS__New(____exports.Vector)
edges[#edges + 1] = __TS__New(____exports.Vector)
normals[#normals + 1] = __TS__New(____exports.Vector)
end
::__continue29::
i = i + 1
end
end
end
self.points = points
self:_recalc()
return self
end
function Polygon.prototype.setAngle(self, radians)
self.angle = radians
self:_recalc()
return self
end
function Polygon.prototype.setOffset(self, offset)
self.offset = offset
self:_recalc()
return self
end
function Polygon.prototype.rotate(self, radians)
local points = self.points
local len = #points
do
local i = 0
while i < len do
points[i + 1]:rotate(radians)
i = i + 1
end
end
self:_recalc()
return self
end
function Polygon.prototype.translate(self, x, y)
local points = self.points
local len = #points
do
local i = 0
while i < len do
local ____points_index_3, ____x_4 = points[i + 1], "x"
____points_index_3[____x_4] = ____points_index_3[____x_4] + x
local ____points_index_5, ____y_6 = points[i + 1], "y"
____points_index_5[____y_6] = ____points_index_5[____y_6] + y
i = i + 1
end
end
self:_recalc()
return self
end
function Polygon.prototype._recalc(self)
local calcPoints = self.calcPoints
local edges = self.edges
local normals = self.normals
local points = self.points
local offset = self.offset
local angle = self.angle
local len = #points
local i
do
i = 0
while i < len do
local calcPoint = calcPoints[i + 1]:copy(points[i + 1])
calcPoint.x = calcPoint.x + offset.x
calcPoint.y = calcPoint.y + offset.y
if angle ~= 0 then
calcPoint:rotate(angle)
end
i = i + 1
end
end
do
i = 0
while i < len do
local p1 = calcPoints[i + 1]
local p2 = i < len - 1 and calcPoints[i + 1 + 1] or calcPoints[1]
local e = edges[i + 1]:copy(p2):sub(p1)
normals[i + 1]:copy(e):perp():normalize()
i = i + 1
end
end
return self
end
function Polygon.prototype.getAABBAsBox(self)
local points = self.calcPoints
local len = #points
local xMin = points[1].x
local yMin = points[1].y
local xMax = points[1].x
local yMax = points[1].y
do
local i = 1
while i < len do
local point = points[i + 1]
if point.x < xMin then
xMin = point.x
elseif point.x > xMax then
xMax = point.x
end
if point.y < yMin then
yMin = point.y
elseif point.y > yMax then
yMax = point.y
end
i = i + 1
end
end
return __TS__New(
____exports.Box,
self.pos:clone():add(__TS__New(____exports.Vector, xMin, yMin)),
xMax - xMin,
yMax - yMin
)
end
function Polygon.prototype.getAABB(self)
return self:getAABBAsBox():toPolygon()
end
function Polygon.prototype.getCentroid(self)
local points = self.calcPoints
local len = #points
local cx = 0
local cy = 0
local ar = 0
do
local i = 0
while i < len do
local p1 = points[i + 1]
local p2 = i == len - 1 and points[1] or points[i + 1 + 1]
local a = p1.x * p2.y - p2.x * p1.y
cx = cx + (p1.x + p2.x) * a
cy = cy + (p1.y + p2.y) * a
ar = ar + a
i = i + 1
end
end
ar = ar * 3
cx = cx / ar
cy = cy / ar
return __TS__New(____exports.Vector, cx, cy)
end
--- An object representing the result of an intersection. Contains:
-- - The two objects participating in the intersection
-- - The vector representing the minimum change necessary to extract the first object
-- from the second one (as well as a unit vector in that direction and the magnitude
-- of the overlap)
-- - Whether the first object is entirely inside the second, and vice versa.
____exports.Response = __TS__Class()
local Response = ____exports.Response
Response.name = "Response"
function Response.prototype.____constructor(self)
self.overlapN = __TS__New(____exports.Vector)
self.overlapV = __TS__New(____exports.Vector)
self.overlap = 2 ^ 1024
self.aInB = true
self.bInA = true
self:clear()
end
function Response.prototype.clear(self)
self.aInB = true
self.bInA = true
self.overlap = 2 ^ 1024
return self
end
T_VECTORS = {}
do
local i = 0
while i < 10 do
T_VECTORS[#T_VECTORS + 1] = __TS__New(____exports.Vector)
i = i + 1
end
end
T_ARRAYS = {}
do
local i = 0
while i < 5 do
T_ARRAYS[#T_ARRAYS + 1] = {}
i = i + 1
end
end
--- Temporary response used for polygon hit detection.
local T_RESPONSE = __TS__New(____exports.Response)
--- Tiny "point" polygon used for polygon hit detection.
local TEST_POINT = __TS__New(
____exports.Box,
__TS__New(____exports.Vector),
0.000001,
0.000001
):toPolygon()
--- Calculates which Voronoi region a point is on a line segment.
-- It is assumed that both the line and the point are relative to `(0,0)`
--
-- | (0) |
-- (-1) [S]--------------[E] (1)
-- | (0) |
--
-- @param line The line segment.
-- @param point The point.
-- @returns LEFT_VORONOI_REGION (-1) if it is the left region,
-- MIDDLE_VORONOI_REGION (0) if it is the middle region,
-- RIGHT_VORONOI_REGION (1) if it is the right region.
function ____exports.voronoiRegion(line, point)
local len2 = line:len2()
local dp = point:dot(line)
if dp < 0 then
return -1
elseif dp > len2 then
return 1
else
return 0
end
end
--- Check if a point is inside a circle.
--
-- @param p The point to test.
-- @param c The circle to test.
-- @returns true if the point is inside the circle, false if it is not.
function ____exports.pointInCircle(p, c)
local differenceV = table.remove(T_VECTORS):copy(p):sub(c.pos):sub(c.offset)
local radiusSq = c.r * c.r
local distanceSq = differenceV:len2()
T_VECTORS[#T_VECTORS + 1] = differenceV
return distanceSq <= radiusSq
end
--- Check if a point is inside a convex polygon.
--
-- @param p The point to test.
-- @param poly The polygon to test.
-- @returns true if the point is inside the polygon, false if it is not.
function ____exports.pointInPolygon(p, poly)
TEST_POINT.pos:copy(p)
T_RESPONSE:clear()
local result = ____exports.testPolygonPolygon(TEST_POINT, poly, T_RESPONSE)
if result then
result = T_RESPONSE.aInB
end
return result
end
--- Check if two circles collide.
--
-- @param a The first circle.
-- @param b The second circle.
-- @param response Response object (optional) that will be populated if the circles intersect.
-- @returns true if the circles intersect, false if they don't.
function ____exports.testCircleCircle(a, b, response)
local differenceV = table.remove(T_VECTORS):copy(b.pos):add(b.offset):sub(a.pos):sub(a.offset)
local totalRadius = a.r + b.r
local totalRadiusSq = totalRadius * totalRadius
local distanceSq = differenceV:len2()
if distanceSq > totalRadiusSq then
T_VECTORS[#T_VECTORS + 1] = differenceV
return false
end
if response then
local dist = math.sqrt(distanceSq)
response.a = a
response.b = b
response.overlap = totalRadius - dist
response.overlapN:copy(differenceV:normalize())
response.overlapV:copy(differenceV):scale(response.overlap)
response.aInB = a.r <= b.r and dist <= b.r - a.r
response.bInA = b.r <= a.r and dist <= a.r - b.r
end
T_VECTORS[#T_VECTORS + 1] = differenceV
return true
end
--- Check if a polygon and a circle collide.
--
-- @param polygon The polygon.
-- @param circle The circle.
-- @param response Response object (optional) that will be populated if they interset.
-- @returns true if they intersect, false if they don't.
function ____exports.testPolygonCircle(polygon, circle, response)
local circlePos = table.remove(T_VECTORS):copy(circle.pos):add(circle.offset):sub(polygon.pos)
local radius = circle.r
local radius2 = radius * radius
local points = polygon.calcPoints
local len = #points
local edge = table.remove(T_VECTORS)
local point = table.remove(T_VECTORS)
do
local i = 0
while i < len do
local next = i == len - 1 and 0 or i + 1
local prev = i == 0 and len - 1 or i - 1
local overlap = 0
local overlapN = nil
edge:copy(polygon.edges[i + 1])
point:copy(circlePos):sub(points[i + 1])
if response and point:len2() > radius2 then
response.aInB = false
end
local region = ____exports.voronoiRegion(edge, point)
if region == -1 then
edge:copy(polygon.edges[prev + 1])
local point2 = table.remove(T_VECTORS):copy(circlePos):sub(points[prev + 1])
region = ____exports.voronoiRegion(edge, point2)
if region == 1 then
local dist = point:len()
if dist > radius then
T_VECTORS[#T_VECTORS + 1] = circlePos
T_VECTORS[#T_VECTORS + 1] = edge
T_VECTORS[#T_VECTORS + 1] = point
T_VECTORS[#T_VECTORS + 1] = point2
return false
elseif response then
response.bInA = false
overlapN = point:normalize()
overlap = radius - dist
end
end
T_VECTORS[#T_VECTORS + 1] = point2
elseif region == 1 then
edge:copy(polygon.edges[next + 1])
point:copy(circlePos):sub(points[next + 1])
region = ____exports.voronoiRegion(edge, point)
if region == -1 then
local dist = point:len()
if dist > radius then
T_VECTORS[#T_VECTORS + 1] = circlePos
T_VECTORS[#T_VECTORS + 1] = edge
T_VECTORS[#T_VECTORS + 1] = point
return false
elseif response then
response.bInA = false
overlapN = point:normalize()
overlap = radius - dist
end
end
else
local normal = edge:perp():normalize()
local dist = point:dot(normal)
local distAbs = math.abs(dist)
if dist > 0 and distAbs > radius then
T_VECTORS[#T_VECTORS + 1] = circlePos
T_VECTORS[#T_VECTORS + 1] = normal
T_VECTORS[#T_VECTORS + 1] = point
return false
elseif response then
overlapN = normal
overlap = radius - dist
if dist >= 0 or overlap < 2 * radius then
response.bInA = false
end
end
end
if overlapN and response and math.abs(overlap) < math.abs(response.overlap) then
response.overlap = overlap
response.overlapN:copy(overlapN)
end
i = i + 1
end
end
if response then
response.a = polygon
response.b = circle
response.overlapV:copy(response.overlapN):scale(response.overlap)
end
T_VECTORS[#T_VECTORS + 1] = circlePos
T_VECTORS[#T_VECTORS + 1] = edge
T_VECTORS[#T_VECTORS + 1] = point
return true
end
--- Check if a circle and a polygon collide.
-- **NOTE:** This is slightly less efficient than polygonCircle as it just
-- runs polygonCircle and reverses everything at the end.
--
-- @param circle The circle.
-- @param polygon The polygon.
-- @param response Response object (optional) that will be populated if they interset.
-- @returns true if they intersect, false if they don't.
function ____exports.testCirclePolygon(circle, polygon, response)
local result = ____exports.testPolygonCircle(polygon, circle, response)
if result and response then
local a = response.a
local aInB = response.aInB
response.overlapN:reverse()
response.overlapV:reverse()
response.a = response.b
response.b = a
response.aInB = response.bInA
response.bInA = aInB
end
return result
end
return ____exports