diff --git a/algo.lua b/algo.lua new file mode 100644 index 0000000..9231aff --- /dev/null +++ b/algo.lua @@ -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 diff --git a/init.lua b/init.lua index c3b7df3..9f1d0db 100644 --- a/init.lua +++ b/init.lua @@ -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') +} diff --git a/meta.lua b/meta.lua new file mode 100644 index 0000000..88b712e --- /dev/null +++ b/meta.lua @@ -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 diff --git a/rect.lua b/rect.lua new file mode 100644 index 0000000..f555082 --- /dev/null +++ b/rect.lua @@ -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 diff --git a/spec/gear_spec.lua b/spec/gear_spec.lua new file mode 100644 index 0000000..b1d8c23 --- /dev/null +++ b/spec/gear_spec.lua @@ -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) diff --git a/strings.lua b/strings.lua new file mode 100644 index 0000000..23ddf79 --- /dev/null +++ b/strings.lua @@ -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 diff --git a/vec.lua b/vec.lua new file mode 100644 index 0000000..50857ca --- /dev/null +++ b/vec.lua @@ -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