[*] Initial commit.

This commit is contained in:
Lorenzo Cogotti 2022-08-12 17:21:42 +02:00
commit 5c345d5e60
6 changed files with 877 additions and 0 deletions

46
.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# ---> Lua
# Compiled Lua sources
luac.out
# luarocks build files
*.src.rock
*.zip
*.tar.gz
# Object files
*.o
*.os
*.ko
*.obj
*.elf
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# ldoc output directory
doc/

17
LICENSE Normal file
View File

@ -0,0 +1,17 @@
Copyright (c) 2022 The DoubleFourteen Code Forge
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.

45
README.md Normal file
View File

@ -0,0 +1,45 @@
df-utils - LÖVE Utility Library
============================================
**df-utils** provides stateless functions for common
[LÖVE](https://love2d.org) game development, including:
* 2D vector algebra
* Minimal 3D vector algebra
* 2D bounds (axis-aligned rectangles)
* General utility math functions
* Common algorithms
Code is reasonably biased towards speed, at the occasional
expense of abstraction.
Documentation
=============
Code is documented with [LDoc](https://github.com/lunarmodules/LDoc).
Documentation may be generated running the command:
```sh
ldoc init.lua
```
`ldoc` generates a `doc` directory, open `doc/index.html`
with your favorite browser to read the documentation.
Test suite
==========
The test suite uses [busted](https://olivinelabs.com/busted/).
Tests may be run with the command:
```sh
lua spec/utils_spec.lua
```
License
=======
See [LICENSE](LICENSE) for details.

28
df-utils-scm-1.rockspec Normal file
View File

@ -0,0 +1,28 @@
rockspec_format = "3.0"
package = "df-utils"
version = "scm-1"
source = {
url = "git+https://git.doublefourteen.io/lua/df-utils.git"
}
description = {
summary = "The DoubleFourteen LÖVE Utility Library",
homepage = "https://git.doublefourteen.io/lua/df-utils",
maintainer = "The DoubleFourteen Code Forge <info@doublefourteen.io>",
license = "zlib",
labels = { "math", "physics", "algorithms", "love", "game" }
}
dependencies = {
"lua >= 5.2"
}
test_dependencies = {
"busted"
}
build = {
type = "builtin",
modules = {
["df-utils"] = "init.lua"
}
}
test = {
type = "busted"
}

527
init.lua Normal file
View File

@ -0,0 +1,527 @@
--- 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

214
spec/utils_spec.lua Normal file
View File

@ -0,0 +1,214 @@
require 'busted.runner'()
describe("df-utils", function()
setup(function()
utils = require 'init'
math = require 'math'
end)
describe("insertion sort #sort", function()
local insertionsort = utils.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 = utils.bsearchl
local bsearchr = utils.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 = utils.pointinrect
local rectempty = utils.rectempty
local recteq = utils.recteq
local rectexpand = utils.rectexpand
local rectinside = utils.rectinside
local rectintersection = utils.rectintersection
local rectunion = utils.rectunion
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)