[spec/*,*] Split library in submodules.

This commit is contained in:
Lorenzo Cogotti 2022-08-16 00:10:09 +02:00
parent e658ba45c7
commit 0e2dd54011
7 changed files with 811 additions and 517 deletions

129
algo.lua Normal file
View File

@ -0,0 +1,129 @@
--- General stateless utility algorithms
--
-- @module gear.algo
-- @copyright 2022 The DoubleFourteen Code Forge
-- @author Lorenzo Cogotti
local floor = math.floor
local min, max = math.min, math.max
local algo = {}
--- 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 algo.clamp(x, a, b) return min(max(x, a), b) end
--- Fast remove from array.
--
-- Replace 'array[i]' with last array's element and
-- discard array's tail.
function algo.removefast(array, i)
local n = #array
array[i] = array[n] -- NOP if i == n
array[n] = nil
end
local function lt(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 algo.insertionsort(array, less)
less = less or lt
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 algo.bsearchl(array, what, less)
less = less or lt
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 algo.bsearchr(array, what, less)
less = less or lt
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
return algo

528
init.lua
View File

@ -1,527 +1,21 @@
--- LÖVE Utility Library
--- LÖVE Utility Gear
--
-- Stateless general purpose functions and utilities.
-- 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
-- @module gear
-- @copyright 2022 The DoubleFourteen Code Forge
-- @author Lorenzo Cogotti
local utils = {}
local BASE = (...)..'.'
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
return {
algo = require(BASE..'algo'),
meta = require(BASE..'meta'),
rect = require(BASE..'rect'),
strings = require(BASE..'strings'),
vec = require(BASE..'vec')
}

38
meta.lua Normal file
View File

@ -0,0 +1,38 @@
--- Functions dealing with metatables and tables merging.
--
-- @module gear.meta
-- @copyright 2022 The DoubleFourteen Code Forge
-- @author Lorenzo Cogotti
local meta = {}
--- Test whether 'obj' is an instance of a given class 'cls'.
function meta.isinstance(obj, cls)
repeat
local m = getmetatable(obj)
if m == cls then return true end
obj = m
until obj == nil
return false
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 meta.mergetable(to, from)
for k,v in pairs(from) do
if to[k] == nil then
to[k] = type(v) == 'table' and meta.mergetable({}, v) or v
end
end
return to
end
return meta

135
rect.lua Normal file
View File

@ -0,0 +1,135 @@
--- Axis-aligned rectangles.
--
-- Function for basic bounding rectangles building and testing.
--
-- @module gear.rect
-- @copyright 2022 The DoubleFourteen Code Forge
-- @author Lorenzo Cogotti
local rotatesincos = require((...):gsub('rect', '')..'vec').rotatesincos
local min, max = math.min, math.max
local abs = math.abs
local rect = {}
--- Extend rectangle to include a point.
function rect.expand(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 union of two rectangles.
function rect.union(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
--- Calculate and return the intersection between two rectangles.
function rect.intersection(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
--- Rotate 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 rect.rotate(rx,ry,rw,rh, rot, ox,oy)
if rw < 0 or rh < 0 then
return rx,ry,rw,rh
end
ox = ox or 0
oy = oy or 0
local sina,cosa = sin(rot),cos(rot)
local x1,y1 = rotatesincos(rx, ry, sina,cosa, ox,oy)
local x2,y2 = rotatesincos(rx+rw, ry, sina,cosa, ox,oy)
local x3,y3 = rotatesincos(rx+rw, ry+rh, sina,cosa, ox,oy)
local x4,y4 = rotatesincos(rx, ry+rh, sina,cosa, ox,oy)
local rxw, rxh
rx = min(min(min(x1, x2), x3), x4)
ry = min(min(min(y1, y2), y3), y4)
rxw = max(max(max(x1, x2), x3), x4)
ryh = max(max(max(y1, y2), y3), y4)
return rx,ry, rxw-rx,ryh-ry
end
--- Test whether point (x,y) lies inside a rectangle.
function rect.pointinside(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 rect.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 rect.eq(x1,y1,w1,h1, x2,y2,w2,h2, eps)
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 rect.isempty(x,y,w,h)
return w < 0 or h < 0
end
return rect

216
spec/gear_spec.lua Normal file
View File

@ -0,0 +1,216 @@
require 'busted.runner'()
describe("gear", function()
setup(function()
gear = require 'init'
algo = gear.algo
rect = gear.rect
math = require 'math'
end)
describe("insertion sort #sort", function()
local insertionsort = algo.insertionsort
it("sorts arrays", function()
local elems = {}
local expect = {}
for n = 2,512 do
for i = 1,n do
elems[i] = math.random(-32768, 32767)
expect[i] = elems[i]
end
table.sort(expect)
insertionsort(elems)
assert.are.same(expect, elems)
end
end)
it("has no effect on single element array", function()
for i = 1,10 do
local val = (math.random() - 0.5) * 1024
local expect = { val }
local elems = { val }
insertionsort(elems)
assert.are.same(expect, elems)
end
end)
it("does nothing on empty array", function()
local expect = {}
local elems = {}
insertionsort(elems)
assert.are.same(expect, elems)
end)
end)
describe("binary search #bsearch", function()
local bsearchl = algo.bsearchl
local bsearchr = algo.bsearchr
it("behaves properly on empty arrays", function()
local empty = {}
local idx = bsearchr(empty, 1)
assert.is_true(empty[idx] == nil)
idx = bsearchl(empty, 1)
assert.is_true(empty[idx] == nil)
end)
it("finds elements within sorted arrays", function()
local dims = { 1, 2, 64, 512, 1024 }
for _,n in ipairs(dims) do
local elems = {}
for i = 1,n do
elems[#elems+1] = ((math.random() - 0.5) * 4096)
end
local mustfind = {}
for i = 1,10 do
mustfind[#mustfind+1] = elems[math.random(1,#elems)]
end
local mustnotfind = {
4097,
-4097,
-math.huge,
math.huge,
16386,
-16386
}
table.sort(elems)
for _,v in ipairs(mustfind) do
local idx = bsearchl(elems, v)
assert.equal(v, elems[idx])
assert.is_false(elems[idx+1] ~= nil and elems[idx+1] <= v)
idx = bsearchr(elems, v)
assert.equal(v, elems[idx])
assert.is_false(elems[idx+1] ~= nil and elems[idx+1] <= v)
end
for _,v in ipairs(mustnotfind) do
local idx = bsearchl(elems, v)
assert.not_equal(v, elems[idx])
if v < elems[1] then
assert.is_true(elems[idx] == nil)
else
assert.is_true(elems[idx] <= v)
end
idx = bsearchr(elems, v)
assert.not_equal(v, elems[idx])
if v > elems[#elems] then
assert.is_true(elems[idx] == nil)
else
assert.is_true(elems[idx] >= v)
end
end
end
end)
end)
describe("rect #bounds", function()
local bigreal = 9999999.0
local min,max = math.min, math.max
local pointinrect = rect.pointinside
local rectempty = rect.isempty
local recteq = rect.eq
local rectexpand = rect.expand
local rectinside = rect.rectinside
local rectintersection = rect.intersection
local rectunion = rect.union
it("is empty if its dimensions are negative", function()
assert.is_true(rectempty(0,0,-1,-1))
assert.is_true(rectempty(bigreal,bigreal,-bigreal,-bigreal))
assert.is_true(rectempty(-bigreal,-bigreal,-bigreal,-bigreal))
assert.is_true(rectempty(0,0,bigreal,-1))
assert.is_true(rectempty(0,0,0,-1))
assert.is_true(rectempty(0,0,-1,bigreal))
assert.is_true(rectempty(0,0,-1,0))
assert.is_false(rectempty(0,0,0,0))
assert.is_false(rectempty(0,0,-0,0))
assert.is_false(rectempty(bigreal,bigreal,bigreal,bigreal))
end)
it("doesn't contain anything if empty", function()
local x,y,w,h = 0,0,-1,-1
assert.is_false(rectinside(0,0,0,0, x,y,w,h))
assert.is_false(rectinside(x,y,w,h, x,y,w,h))
assert.is_false(rectinside(0,0,bigreal,bigreal, x,y,w,h))
assert.is_false(rectinside(0,0,-bigreal,-bigreal, x,y,w,h))
end)
it("always contains empty rect if non-empty", function()
assert.is_true(rectinside(0,0,-1,-1, 0,0,0,0))
assert.is_true(rectinside(bigreal,bigreal,-bigreal,-bigreal, 0,0,0,0))
assert.is_true(rectinside(bigreal,bigreal,bigreal,-bigreal, 0,0,0,0))
assert.is_true(rectinside(bigreal,bigreal,-bigreal,bigreal, 0,0,0,0))
assert.is_true(rectinside(bigreal,bigreal,-1.0e-5,bigreal, 0,0,0,0))
end)
it("may contain a single point", function()
for i,pt in ipairs({{0,0}, {bigreal,bigreal}, {-bigreal,-bigreal}}) do
local px,py = pt[1], pt[2]
local x,y,w,h = 0,0,-1,-1
x,y,w,h = rectexpand(x,y,w,h, px,py)
assert.is_false(rectempty(x,y,w,h))
assert.is_true(pointinrect(px,py, x,y,w,h))
assert.is_true(recteq(x,y,w,h, px,py,0,0))
end
end)
it("may expand arbitrarily to contain more points", function()
local points = {}
local xmin,ymin = math.huge, math.huge
local xmax,ymax = -math.huge,-math.huge
for i = 1,16535 do
local x = (math.random() - 0.5) * 50
local y = (math.random() - 0.5) * 50
points[#points+1] = { x, y }
xmin = min(xmin, x)
ymin = min(ymin, y)
xmax = max(xmax, x)
ymax = max(ymax, y)
end
local x,y,w,h = 0,0,-1,-1
for i,pt in ipairs(points) do
local px,py = pt[1], pt[2]
x,y,w,h = rectexpand(x,y,w,h, px,py)
assert.is_true(pointinrect(px,py, x,y,w,h))
end
local ex,ey,ew,eh = xmin,ymin, xmax-xmin,ymax-ymin
assert.is_false(rectempty(x,y,w,h))
assert.is_true(recteq(x,y,w,h, ex,ey,ew,eh, 0.1))
end)
it("may expand arbitrarily to contain more rects", function()
pending("to be tested...")
end)
it("may be used to enclose arbitrary geometry", function()
pending("to be tested...")
end)
it("may be tested against other rects", function()
pending("to be tested...")
end)
it("may be intersected with other rects", function()
pending("to be tested...")
end)
end)
end)

28
strings.lua Normal file
View File

@ -0,0 +1,28 @@
local strings = {}
--- Test whether a string starts with a prefix.
function strings.startswith(s, prefix)
-- optimized version of: return s:sub(1, #prefix) == prefix
for i = 1,#prefix do
if s:byte(i) ~= prefix:byte(i) then
return false
end
end
return true
end
--- Test whether a string ends with a trailing suffix.
function strings.endswith(s, trailing)
-- optimized version of: return trailing == "" or s:sub(-#trailing) == trailing
local n1,n2 = #s,#trailing
for i = 0,n2-1 do
if s:byte(n1-i) ~= trailing:byte(n2-i) then
return false
end
end
return true
end
return strings

254
vec.lua Normal file
View File

@ -0,0 +1,254 @@
--- Vector algebra.
--
-- Functions implementing basic 2D and 3D vector algebra.
-- Code is reasonably optimized for speed.
--
-- @module gear.vec
-- @copyright 2022 The DoubleFourteen Code Forge
-- @author Lorenzo Cogotti
local min, max = math.min, math.max
local sin, cos = math.sin, math.cos
local abs = math.abs
local sqrt = math.sqrt
local vec = {}
--- Vector dot product.
function vec.dot(x1,y1, x2,y2)
return x1*x2 + y1*y2
end
--- vec.dot() equivalent for 3D vector.
function vec.dot3(x1,y1,z1, x2,y2,z2)
return x1*x2 + y1*y2 + z1*z2
end
--- Vector squared length.
function vec.sqrlen(x,y)
return x*x + y*y -- vec.dot(x,y, x,y)
end
--- vec.sqrlen() equivalent for 3D vectors.
function vec.sqrlen3(x,y,z)
return x*x + y*y + z*z
end
--- Vector length.
function vec.len(x,y)
return sqrt(x*x + y*y) -- sqrt(vec.sqrlen(x,y))
end
--- vec.len() equivalent for 3D vectors.
function vec.len3(x,y,z)
return sqrt(x*x + y*y + z*z)
end
--- Vector addition.
function vec.add(x1,y1, x2,y2)
return x1+x2, y1+y2
end
--- vec.add() equivalent for 3D vectors.
function vec.add3(x1,y1,z1, x2,y2,z2)
return x1+x2, y1+y2, z1+z2
end
--- Vector subtraction.
function vec.sub(x1,y1, x2,y2)
return x1-x2, y1-y2
end
--- vec.sub() equivalent for 3D vectors.
function vec.sub3(x1,y1,z1, x2,y2,z2)
return x1-x2, y1-y2, z1-z2
end
--- Vector scale.
function vec.scale(x,y, s)
return x*s, y*s
end
--- vec.scale() equivalent for 3D vectors.
function vec.scale3(x,y,z, s)
return x*s, y*s, z*s
end
--- Vector division by scalar.
function vec.div(x,y, s)
return x/s, y/s
end
--- vec.div() equivalent for 3D vectors.
function vec.div3(x,y,z, s)
return x/s, y/s, z/s
end
--- Vector multiply add.
--
-- @return the first vector, added to the second vector scaled by a factor.
function vec.madd(x1,y1, s, x2,y2)
return x1 + x2*s, y1 + y2*s
end
--- vec.madd() equivalent for 3D vectors.
function vec.madd3(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 vec.eq(x1,y1, x2,y2, eps)
eps = eps or 0.001
return abs(x1-x2) < eps and abs(y1-y2) < eps
end
--- vec.eq() equivalent for 3D vectors.
function vec.eq3(x1,y1,z1, x2,y2,z2, eps)
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 vec.normalize(x,y)
local len = sqrt(x*x + y*y) -- vec.len(x,y)
if len < 1.0e-4 then
return x,y, 0
end
return x / len, y / len, len
end
--- vec.normalize() equivalent for 3D vectors.
function vec.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 vec.sqrdist(x1,y1, x2,y2)
local dx,dy = x2-x1, y2-y1
return dx*dx, dy*dy -- vec.sqrlen(dx,dy)
end
--- vec.sqrdist() equivalent for 3D vectors.
function vec.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 vec.dist(x1,y1, x2,y2)
local dx,dy = x2-x1, y2-y1
return sqrt(dx*dx + dy*dy) -- sqrt(vec.sqrdist(x1,y1, x2,y2))
end
--- vec.dist() equivalent for 3D vectors/points.
function vec.dist3(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
--- Rotate point (px,py) around (ox,oy) by the provided
-- sine and cosine.
--
-- This function should only be used for (valuable)
-- optimization purposes.
function vec.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 vec.rotatepoint(px,py, rot, ox,oy)
ox = ox or 0
oy = oy or 0
local sina,cosa = sin(rot),cos(rot)
-- vec.rotatesincos(px,py, sina,cosa, ox,oy)
return ox + cosa*px - sina*py,
oy + sina*px + cosa*py
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 vec.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 vec.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 vec