gear/init.lua

528 lines
14 KiB
Lua

--- LÖVE Utility Library
--
-- Stateless general purpose functions and utilities.
-- Provides various basic functionality, including: general utility
-- algorithms, linear algebra and simple bounds checking functions.
-- Code is reasonably optimized for speed.
--
-- @module df-utils
-- @copyright 2022 The DoubleFourteen Code Forge
-- @author Lorenzo Cogotti
local utils = {}
local floor = math.floor
local min, max = math.min, math.max
local sin, cos = math.sin, math.cos
local sqrt = math.sqrt
--- Clamp x within range [a,b] (where b >= a).
--
-- @param x (number) value to clamp.
-- @param a (number) interval lower bound (inclusive).
-- @param b (number) interval upper bound (includive).
-- @return (number) clamped value.
function utils.clamp(x, a, b) return min(max(x, a), b) end
--- Test whether a string starts with a prefix.
function utils.startswith(s, prefix)
return s:sub(1, #prefix) == prefix
end
--- Test whether a string ends with a trailing suffix.
function utils.endswith(s, trailing)
return trailing == "" or s:sub(-#trailing) == trailing
end
--- Merge table 'from' into table 'to'.
--
-- For every field in 'from', copy it to destination
-- table 'to', whenever the same field is nil in that table.
--
-- The same process is applied recursively to sub-tables.
function utils.mergetable(to, from)
for k,v in pairs(from) do
if to[k] == nil then
to[k] = type(v) == 'table' and utils.mergetable({}, v) or v
end
end
return to
end
--- Test whether 'obj' is an instance of a given class 'cls'.
function utils.isinstance(obj, cls)
repeat
local m = getmetatable(obj)
if m == cls then return true end
obj = m
until obj == nil
return false
end
local function opless(a, b) return a < b end
--- Sort array using Insertion Sort - O(n^2).
--
-- Provides the most basic sorting algorithm around.
-- Performs better than regular table.sort() for small arrays
-- (~100 elements).
--
-- @param array (table) array to be sorted.
-- @param less (function|nil) comparison function, takes 2 arguments,
-- returns true if its first argument is less than its second argument, false otherwise.
-- Defaults to operator <.
function utils.insertionsort(array, less)
less = less or opless
for i = 2,#array do
local val = array[i]
local j = i
while j > 1 and less(val, array[j-1]) do
array[j] = array[j-1]
j = j - 1
end
array[j] = val
end
end
--- Binary search last element where
-- what <= array[i] - also known as lower bound.
--
-- @param array (table) an array sorted according to the less function.
-- @param what the comparison argument.
-- @param less (function|nil) sorting criterium, a function taking 2 arguments,
-- returns true if the first argument is less than the second argument,
-- false otherwise. Defaults to using the < operator.
--
-- @return (number) the greatest index i, where what <= array[i].
-- If no such element exists, it returns an out of bounds index
-- such that array[i] == nil.
function utils.bsearchl(array, what, less)
less = less or opless
local lo, hi = 1, #array
local ofs, mid = -1, hi
while mid > 0 do
mid = floor(hi / 2)
-- array[lo+mid] <= what <-> what >= array[lo+mid]
-- <-> not what < array[lo+mid]
if not less(what, array[lo+mid]) then
lo = lo + mid
ofs = 0 -- at least one element where array[lo+mid] <= what
end
hi = hi - mid
end
return lo + ofs
end
--- Binary search first element where
-- what >= array[i] - also known as upper bound.
--
-- @param array (array) an array sorted according to the less function.
-- @param what the comparison argument.
-- @param less (function|nil) sorting criterium, a function taking 2 arguments,
-- returns true if the first argument is less than the second argument,
-- false otherwise. Defaults to using the < operator.
--
-- @return (number) the smallest index i, where what >= array[i].
-- If no such element exists, it returns an out of bounds index
-- such that array[i] == nil.
function utils.bsearchr(array, what, less)
less = less or opless
local lo, hi = 1, #array
local ofs, mid = -1, hi
while mid > 0 do
mid = floor(hi / 2)
-- array[lo+mid] >= what <-> not array[lo+mid] < what
if not less(array[lo+mid], what) then
ofs = 0
else
lo = lo + mid
ofs = 1
end
hi = hi - mid
end
return lo + ofs
end
--- Fast remove from array.
--
-- Replace 'array[i]' with last array's element and
-- discard array's tail.
function utils.removefast(array, i)
local n = #array
array[i] = array[n] -- NOP if i == n
array[n] = nil
end
--- Vector dot product.
function utils.dot(x1,y1, x2,y2)
return x1*x2 + y1*y2
end
--- utils.dot() equivalent for 3D vector.
function utils.dot3(x1,y1,z1, x2,y2,z2)
return x1*x2 + y1*y2 + z1*z2
end
--- Vector squared length.
function utils.vecsqrlen(x,y)
return x*x + y*y -- utils.dot(x,y, x,y)
end
--- utils.vecsqrlen() equivalent for 3D vectors.
function utils.vecsqrlen3(x,y,z)
return x*x + y*y + z*z
end
--- Vector length.
function utils.veclen(x,y)
return sqrt(x*x + y*y) -- sqrt(utils.vecsqrlen(x,y))
end
--- utils.veclen() equivalent for 3D vectors.
function utils.veclen3(x,y,z)
return sqrt(x*x + y*y + z*z)
end
--- Vector addition (inline this function when possible).
function utils.vecadd(x1,y1, x2,y2)
return x1+x2, y1+y2
end
--- utils.vecadd() equivalent for 3D vectors.
function utils.vecadd3(x1,y1,z1, x2,y2,z2)
return x1+x2, y1+y2, z1+z2
end
--- Vector subtraction (inline this function when possible).
function utils.vecsub(x1,y1, x2,y2)
return x1-x2, y1-y2
end
--- utils.vecsub() equivalent for 3D vectors.
function utils.vecsub3(x1,y1,z1, x2,y2,z2)
return x1-x2, y1-y2, z1-z2
end
--- Vector scale (inline this function when possible).
function utils.vecscale(x,y, s)
return x*s, y*s
end
--- utils.vecscale() equivalent for 3D vectors.
function utils.vecscale3(x,y,z, s)
return x*s, y*s, z*s
end
--- Vector division by scalar (inline this function when possible).
function utils.vecdiv(x,y, s)
return x/s, y/s
end
--- utils.vecdiv() equivalent for 3D vectors.
function utils.vecdiv3(x,y,z, s)
return x/s, y/s, z/s
end
--- Vector fused multiply add (inline this function when possible).
--
-- @return the first vector, added to the second vector scaled by a factor.
function utils.vecma(x1,y1, s, x2,y2)
return x1 + x2*s, y1 + y2*s
end
--- utils.vecma() equivalent for 3D vectors.
function utils.vecma3(x1,y1,z1, s, x2,y2,z2)
return x1 + x2*s, y1 + y2*s, z1 + z2*s
end
--- Test vectors for equality with optional epsilon.
function utils.veceq(x1,y1, x2,y2, eps)
local abs = math.abs
eps = eps or 0.001
return abs(x1-x2) < eps and abs(y1-y2) < eps
end
--- utils.veceq() equivalent for 3D vectors.
function utils.veceq3(x1,y1,z1, x2,y2,z2, eps)
local abs = math.abs
eps = eps or 0.001
return abs(x1-x2) < eps and
abs(y1-y2) < eps and
abs(z1-z2) < eps
end
--- Normalize vector.
--
-- @return (x,y, len) normalized components and vector's original length.
function utils.normalize(x,y)
local len = sqrt(x*x + y*y) -- utils.veclen(x,y)
if len < 1.0e-4 then
return x,y, 0
end
return x / len, y / len, len
end
--- utils.normalize() equivalent for 3D vectors.
function utils.normalize3(x,y,z)
local len = sqrt(x*x + y*y + z*z)
if len < 1.0e-4 then
return x,y,z, 0
end
return x / len, y / len, z / len, len
end
--- Calculate the squared distance between two vectors/points.
function utils.sqrdist(x1,y1, x2,y2)
local dx,dy = x2-x1, y2-y1
return dx*dx, dy*dy -- utils.vecsqrlen(dx,dy)
end
--- utils.sqrdist() equivalent for 3D vectors.
function utils.sqrdist3(x1,y1,z1, x2,y2,z2)
local dx,dy,dz = x2-x1, y2-y1, z2-z1
return dx*dx, dy*dy, dz*dz
end
--- Calculate the distance between two vectors/points.
function utils.distance(x1,y1, x2,y2)
local dx,dy = x2-x1, y2-y1
return sqrt(dx*dx + dy*dy) -- sqrt(utils.sqrdist(x1,y1, x2,y2))
end
--- utils.distance() equivalent for 3D vectors/points.
function utils.distance3(x1,y1,z1, x2,y2,z2)
local dx,dy,dz = x2-x1, y2-y1, z2-z1
return sqrt(dx*dx + dy*dy + dz*dz)
end
--- Calculate and return the union of two rectangles.
function utils.rectunion(x1,y1,w1,h1, x2,y2,w2,h2)
local xw1,yh1, xw2,yh2
if w1 < 0 or h1 < 0 then
local huge = math.huge
x1,y1,xw1,yh1 = huge,huge,-huge,-huge
else
xw1,yh1 = x1 + w1,y1 + h1
end
if w2 < 0 or h2 < 0 then
local huge = math.huge
x2,y2,xw2,yh2 = huge,huge,-huge,-huge
else
xw2,yh2 = x2 + w2,y2 + h2
end
x1 = min(x1, x2)
y1 = min(y1, y2)
xw1 = max(xw1, xw2)
xh1 = max(xh1, xh2)
return x1, y1, xw1 - x1, yh1 - y1
end
--- Extend rectangle to include a point.
function utils.rectexpand(x,y,w,h, px,py)
if w < 0 or h < 0 then
return px,py,0,0
end
local xw, yh
x = min(x, px)
y = min(y, py)
xw = max(x+w, px)
yh = max(y+h, py)
return x, y, xw-x, yh-y
end
--- Calculate and return the intersection between two rectangles.
function utils.rectintersection(x1,y1,w1,h1, x2,y2,w2,h2)
if w1 < 0 or h1 < 0 then
return x1,y1,w1,h1
elseif w2 < 0 or h2 < 0 then
return x2,y2,w2,h2
end
local xw1,yh1 = x1+w1, y1+h1
local xw2,yh2 = x2+w2, y2+h2
x1 = max(x1, x2)
y1 = max(y1, y2)
xw1 = min(xw1, xw2)
yh1 = min(yh1, yh2)
return x1,y1, xw1-x1,yh1-y1
end
--- Test whether point (x,y) lies inside a rectangle.
function utils.pointinrect(x,y, rx,ry,rw,rh)
return x >= rx and y >= ry and x-rx <= rw and y-ry <= rh
end
--- Test whether the first rectangle lies inside the second.
function utils.rectinside(x1,y1,w1,h1, x2,y2,w2,h2)
return (x1 >= x2 and y1 >= y2 and w1 <= w2 and h1 <= h2 and w2 >= 0 and h2 >= 0)
or ((w1 < 0 or h1 < 0) and (w2 >= 0 and h2 >= 0))
end
--- Test two rectangles for equality with optional epsilon.
function utils.recteq(x1,y1,w1,h1, x2,y2,w2,h2, eps)
local abs = math.abs
eps = eps or 0.007
return (abs(x1 - x2) <= eps and
abs(y1 - y2) <= eps and
abs(w1 - w2) <= eps and
abs(h1 - h2) <= eps)
or ((w1 < 0 or h1 < 0) and (w2 < 0 or h2 < 0))
end
--- Test whether a rectangle is empty.
function utils.rectempty(x,y,w,h)
return w < 0 or h < 0
end
local function rotatesincos(px,py, sina,cosa, ox,oy)
return ox + cosa*px - sina*py,
oy + sina*px + cosa*py
end
--- Rotate point (px,py) around (ox,oy) about rot radians.
function utils.rotatepoint(px,py, rot, ox,oy)
ox = ox or 0
oy = oy or 0
local sina,cosa = sin(rot), cos(rot)
return rotatesincos(px,py, sina,cosa, ox,oy)
end
--- Rotate an axis-aligned rectangle around (ox,oy) about rot radians,
-- and return the result's minimum enclosing
-- axis-aligned rectangle.
--
-- NOTE: This causes precision loss, possibly generating
-- larger bounds than needed for the rotated geometry
-- Don't use this function repeatedly on the same bounds.
function utils.rotatebounds(bx,by,bw,bh, rot, ox,oy)
if bw < 0 or bh < 0 then
return bx,by,bw,bh
end
ox = ox or 0
oy = oy or 0
local sina,cosa = sin(rot), cos(rot)
local x1,y1 = rotatesincos(bx, by, sina,cosa, ox,oy)
local x2,y2 = rotatesincos(bx+bw, by, sina,cosa, ox,oy)
local x3,y3 = rotatesincos(bx+bw, by+bh, sina,cosa, ox,oy)
local x4,y4 = rotatesincos(bx, by+bh, sina,cosa, ox,oy)
local rx = min(min(min(x1, x2), x3), x4)
local ry = min(min(min(y1, y2), y3), y4)
local rxw = max(max(max(x1, x2), x3), x4)
local ryh = max(max(max(y1, y2), y3), y4)
return rx,ry, rxw-rx,ryh-ry
end
--- Transform world coordinates to screen coordinates.
--
-- @param x (number) World coordinate X.
-- @param y (number) World coordinate Y.
-- @param vx (number|nil) Point of view X coordinate, defaults to w/2.
-- @param vy (number|nil) Point of view Y coordinate, defaults to h/2.
-- @param rot (number|nil) View rotation in radians, defaults to 0.
-- @param scale (number|nil) View scale (zoom), defaults to 1.
-- @param left (number|nil) Viewport left corner, defaults to 0.
-- @param top (number|nil) Viewport top corner, defaults to 0.
-- @param w (number|nil) Viewport width, defaults to love.graphics.getWidth().
-- @param h (number|nil) Viewport height, defaults to love.graphics.getHeight().
--
-- @return (x,y) Transformed to screen coordinates according to
-- viewport and offset.
function utils.toscreencoords(x,y, vx,vy, rot, scale, left,top, w,h)
left,top = left or 0, top or 0
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
local halfw,halfh = w/2, h/2
vx,vy = vx or halfw, vy or halfh
rot = rot or 0
scale = scale or 1
local sina,cosa = sin(rot), cos(rot)
x,y = x - vx, y - vy
x,y = cosa*x - sina*y, sina*x + cosa*y
return x*scale + halfw + left, y*scale + halfh + top
end
--- Transform screen coordinates to world coordinates.
--
-- @param x (number) Screen coordinate X.
-- @param y (number) Screen coordinate Y.
-- @param vx (number|nil) Point of view X coordinate, defaults to w/2.
-- @param vy (number|nil) Point of view Y coordinate, defaults to h/2.
-- @param rot (number|nil) View rotation in radians, defaults to 0.
-- @param scale (number|nil) View scale (zoom), defaults to 1.
-- @param left (number|nil) Viewport left corner, defaults to 0.
-- @param top (number|nil) Viewport top corner, defaults to 0.
-- @param w (number|nil) Viewport width, defaults to love.graphics.getWidth().
-- @param h (number|nil) Viewport height, defaults to love.graphics.getHeight().
--
-- @return (x,y) Transformed to world coordinates according to
-- viewport and offset.
function utils.toworldcoords(x,y, vx,vy, rot, scale, left,top,w,h)
left, top = left or 0, top or 0
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
local halfw,halfh = w/2, h/2
vx,vy = vx or halfw, vy or halfh
rot = rot or 0
scale = scale or 1
local sina,cosa = sin(-rot), cos(-rot)
x,y = (x - halfw - left) / scale, (y - halfh - top) / scale
x,y = cosa*x - sina*y, sina*x + cosa*y
return x+vx, y+vy
end
return utils