--- 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