From 2f470cf7c788231e617ba255a4b2ce502738483f Mon Sep 17 00:00:00 2001 From: Max Cahill <1bardesign@gmail.com> Date: Wed, 29 Jan 2020 14:26:28 +1100 Subject: [PATCH] initial commit --- functional.lua | 407 ++++++++++++++++++++++++++++++++ init.lua | 28 +++ intersect.lua | 389 +++++++++++++++++++++++++++++++ math.lua | 212 +++++++++++++++++ oo.lua | 43 ++++ stable_sort.lua | 152 ++++++++++++ state_machine.lua | 135 +++++++++++ table.lua | 108 +++++++++ vec2.lua | 579 ++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 2053 insertions(+) create mode 100644 functional.lua create mode 100644 init.lua create mode 100644 intersect.lua create mode 100644 math.lua create mode 100644 oo.lua create mode 100644 stable_sort.lua create mode 100644 state_machine.lua create mode 100644 table.lua create mode 100644 vec2.lua diff --git a/functional.lua b/functional.lua new file mode 100644 index 0000000..5752386 --- /dev/null +++ b/functional.lua @@ -0,0 +1,407 @@ +--[[ + functional programming facilities +]] + +--collect all keys of a table into a sequence +function table.keys(t) + local r = {} + for k,v in pairs(t) do + table.insert(r, k) + end + return r +end + +--collect all values of a table into a sequence +--(shallow copy if it's already a sequence) +function table.values(t) + local r = sequence:new() + for k,v in pairs(t) do + table.insert(r, v) + end + return r +end + +--simple sequential iteration, f is called for all elements of t +--f can return non-nil to break the loop (and return the value) +function table.foreach(t, f) + for i,v in ipairs(t) do + local r = f(v, i) + if r ~= nil then + return r + end + end +end + +--performs a left to right reduction of t using f, with o as the initial value +-- reduce({1, 2, 3}, f, 0) -> f(f(f(0, 1), 2), 3) +-- (but performed iteratively, so no stack smashing) +function table.reduce(t, f, o) + for i,v in ipairs(t) do + o = f(o, v) + end + return o +end + +--maps a sequence {a, b, c} -> {f(a), f(b), f(c)} +-- (automatically drops any nils due to table.insert, which can be used to simultaneously map and filter) +function table.map(t, f) + local r = {} + for i,v in ipairs(t) do + local mapped = f(v, i) + if mapped ~= nil then + table.insert(r, mapped) + end + end + return r +end + +--filters a sequence +function table.filter(t, f) + local r = {} + for i,v in ipairs(t) do + if f(v, i) then + table.insert(r, v) + end + end + return r +end + +--partitions a sequence based on filter criteria +function table.partition(t, f) + local a = {} + local b = {} + for i,v in ipairs(t) do + if f(v, i) then + table.insert(a, v) + else + table.insert(b, v) + end + end + return a, b +end + +--zips two sequences together into a new table, based on another function +--iteration limited by min(#t1, #t2) +--function receives arguments (t1, t2, i) +--nil results ignored +function table.zip(t1, t2, f) + local ret = {} + local limit = math.min(#t2, #t2) + for i=1, limit do + local v1 = t1[i] + local v2 = t2[i] + local zipped = f(v1, v2, i) + if zipped ~= nil then + table.insert(ret, zipped) + end + end + return ret +end + +--return a copy of a sequence with all duplicates removed +-- causes a little "extra" gc churn; one table and one closure +-- as well as the copied deduped table +function table.dedupe(t) + local seen = {} + return table.filter(t, function(v) + if seen[v] then + return false + end + seen[v] = true + return true + end) +end + +--append sequence t2 into t1, modifying t1 +function table.append_inplace(t1, t2) + table.foreach(t2, function(v) + table.insert(t1, v) + end) + return t1 +end + +--return a new sequence with the elements of both t1 and t2 +function table.append(t1, t2) + local r = {} + append_inplace(r, t1) + append_inplace(r, t2) + return r +end + +--copy a table +-- if deep specified: +-- calls copy method of member directly if it exists +-- and recurses into all "normal" table children +function table.copy(t, deep) + local r = {} + for k,v in pairs(t) do + if deep and type(v) == "table" then + if type(v.copy) == "function" then + v = v:copy() + else + v = table.copy(v, deep) + end + end + r[k] = v + end + return r +end + +----------------------------------------------------------- +--common queries and reductions +----------------------------------------------------------- + +--true if any element of the table matches f +function table.any(t, f) + for i,v in ipairs(t) do + if f(v) then + return true + end + end + return false +end + +--true if no element of the table matches f +function table.none(t, f) + for i,v in ipairs(t) do + if f(v) then + return false + end + end + return true +end + +--true if all elements of the table match f +function table.all(t, f) + for i,v in ipairs(t) do + if not f(v) then + return false + end + end + return true +end + +--counts the elements of t that match f +function table.count(t, f) + local c = 0 + for i,v in ipairs(t) do + if f(v) then + c = c + 1 + end + end + return c +end + +--true if the table contains element e +function table.contains(t, e) + for i, v in ipairs(t) do + if v == e then + return true + end + end + return false +end + +--return the numeric sum of all elements of t +function table.sum(t) + return table.reduce(t, function(a, b) + return a + b + end, 0) +end + +--return the numeric mean of all elements of t +function table.mean(t) + local len = #t + if len == 0 then + return 0 + end + return table.sum(t) / len +end + +--return the minimum and maximum of t in one pass +function table.minmax(t) + local a = table.reduce(t, function(a, b) + a.min = a.min and math.min(a.min, b) or b + a.max = a.max and math.max(a.max, b) or b + return a + end, {}) + if a.min == nil then + a.min = 0 + a.max = 0 + end + return a.min, a.max +end + +function table.max(t) + local min, max = table.minmax(t) + return max +end + +function table.min(t) + local min, max = table.minmax(t) + return min +end + +--return the element of the table that results in the greatest numeric value +--(function receives element and key respectively, table evaluated in pairs order) +function table.find_best(t, f) + local current = nil + local current_best = -math.huge + for k,e in pairs(t) do + local v = f(e, k) + if v > current_best then + current_best = v + current = e + end + end + return current +end + +--return the element of the table that results in the value nearest to the passed value +function table.find_nearest(t, f, v) + return table.find_best(t, function(e) + return -math.abs(f(e) - v) + end) +end + +--return the first element of the table that results in a true filter +function table.find_match(t, f) + for i,v in ipairs(t) do + if f(v) then + return v + end + end + return nil +end + +----------------------------------------------------------- +--sequence - functional wrapper for ordered tables +----------------------------------------------------------- +sequence = {} + +sequence.mt = {__index = sequence} + +--upgrade a table into a functional sequence +function sequence:new(t) + return setmetatable(t or {}, sequence.mt) +end + +--import table functions to sequence as-is +sequence.insert = table.insert +sequence.remove = table.remove +sequence.concat = table.concat +sequence.join = table.concat --alias + +--sorting +sequence.sort = table.stable_sort --(default stable) +sequence.stable_sort = table.stable_sort +sequence.unstable_sort = table.sort + +--import functional interface to sequence +function sequence:keys() + return sequence:new(table.keys(self)) +end + +function sequence:values() + return sequence:new(table.values(self)) +end + +function sequence:foreach(f) + return table.foreach(self, f) +end + +function sequence:reduce(f, o) + return table.foreach(self, f, o) +end + +function sequence:map(f) + return sequence:new(table.map(self, f)) +end + +function sequence:filter(f) + return sequence:new(table.filter(self, f)) +end + +function sequence:partition(f) + local a, b = table.partition(self, f) + return sequence:new(a), sequence:new(b) +end + +function sequence:zip(other, f) + return sequence:new(table.zip(self, other, f)) +end + +function sequence:dedupe() + return table.dedupe(self) +end + +function sequence:append_inplace(other) + return table.append_inplace(self, other) +end + +function sequence:append(other) + return sequence:new():append_inplace(self):append_inplace(other) +end + +function sequence:copy(deep) + return sequence:new(table.copy(self, deep)) +end + +-- + +sequence.any = table.any +sequence.none = table.none +sequence.all = table.all +sequence.count = table.count +sequence.contains = table.contains +sequence.sum = table.sum +sequence.mean = table.mean +sequence.minmax = table.minmax +sequence.max = table.max +sequence.min = table.min +sequence.find_best = table.find_best +sequence.find_nearest = table.find_nearest +sequence.find_match = table.find_match + +--generate a mapping from unique values to plain numbers +--useful for arbitrarily ordering things that don't have +--a natural ordering implied (eg textures for batching) + +unique_mapping = {} +unique_mapping.mt = { + __index = unique_mapping, + __mode = "kv", --weak refs +} + +--(used as storage for non-weak data) +local _MAP_VARS = setmetatable({}, { + __mode = "k" --only keys are weak +}) + +function unique_mapping:new() + local r = setmetatable({}, unique_mapping.mt) + --set up the actual vars + _MAP_VARS[r] = { + current_index = 0, + } + return r +end + +function unique_mapping:_increment() + local vars = _MAP_VARS[self] + vars.current_index = vars.current_index + 1 + return vars.current_index +end + +function unique_mapping:map(value) + local val = self[value] + if val then + return val + end + local i = self:_increment() + self[value] = i + return i +end + + + diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..d3dbdff --- /dev/null +++ b/init.lua @@ -0,0 +1,28 @@ +--[[ + core modules + + if required as the "entire library" (ie by this file), puts everything into + global namespace as it'll presumably be commonly used + + if not, several of the modules work as "normal" modules and return a table + for local-friendly use +]] + +local path = ... +local function relative_file(p) + return table.concat({path, p}, ".") +end + +require(relative_file("oo")) + +require(relative_file("math")) + +require(relative_file("table")) +require(relative_file("stable_sort")) + +require(relative_file("functional")) + +vec2 = require(relative_file("vec2")) +intersect = require(relative_file("intersect")) + +state_machine = require(relative_file("state_machine")) diff --git a/intersect.lua b/intersect.lua new file mode 100644 index 0000000..d2ac071 --- /dev/null +++ b/intersect.lua @@ -0,0 +1,389 @@ +--[[ + geometric intersection routines + from simple point tests to shape vs shape tests + + options for boolean or minimum separating vector results + + continuous sweeps (where provided) also return the + time-domain position of first intersection + + TODO: refactor storage to be pooled rather than fully local + so these functions can be reentrant +]] + +local intersect = {} + +--epsilon for collisions +local COLLIDE_EPS = 1e-6 + +------------------------------------------------------------------------------ +-- circles + +function intersect.circle_point_overlap(pos, rad, v) + return pos:distance_squared(v) < rad * rad +end + +function intersect.circle_circle_overlap(a_pos, a_rad, b_pos, b_rad) + local rad = a_rad + b_rad + return a_pos:distance_squared(b_pos) < rad * rad +end + +local _ccc_delta = vec2:zero() +function intersect.circle_circle_collide(a_pos, a_rad, b_pos, b_rad, into) + --get delta + _ccc_delta:vset(a_pos):vsubi(b_pos) + --squared threshold + local rad = a_rad + b_rad + local dist = _ccc_delta:length_squared() + if dist < rad * rad then + if dist == 0 then + --singular case; just resolve vertically + dist = 1 + _ccc_delta:sset(0,1) + else + --get actual distance + dist = math.sqrt(dist) + end + --allocate if needed + if into == nil then + into = vec2:zero() + end + --normalise, scale to separating distance + into:vset(_ccc_delta):sdiv(dist):smuli(rad - dist) + return into + end + return false +end + +------------------------------------------------------------------------------ +-- line segments +-- todo: separate double-sided, one-sided, and pull-through (along normal) collisions? + +--vector from line seg to point +function intersect._line_to_point(a_start, a_end, b_pos, into) + if into == nil then into = vec2:zero() end + --direction of line + into:vset(a_end):vsub(a_start) + --detect degenerate case + if into:length_squared() <= COLLIDE_EPS then + return intersect.circle_circle_collide(a_start, a_rad, b_pos, b_rad) + end + --solve for factor along line + local dx = (b_pos.x - a_start.x) * (a_end.x - a_start.x) + local dy = (b_pos.y - a_start.y) * (a_end.y - a_start.y) + local u = (dx + dy) / into:length_squared() + --clamp onto segment + u = math.clamp01(u) + --get the displacement to the nearest point (invalidate direction) + into:smuli(u):vaddi(a_start):vsubi(b_pos) + return into +end + +--line displacement vector from separation vector +function intersect._line_displacement_to_sep(a_start, a_end, separation, total_rad) + local distance = separation:normalisei_len() + local sep = distance - total_rad + if sep <= 0 then + if distance <= COLLIDE_EPS then + --point intersecting the line; push out along normal + separation:vset(a_end):vsub(a_start):normalisei():rot90li() + else + separation:smuli(-sep) + end + return separation + end + return false +end + +--collide a line segment with a circle +function intersect.line_circle_collide(a_start, a_end, a_rad, b_pos, b_rad, into) + into = intersect._line_to_point(a_start, a_end, b_pos, into) + return intersect._line_displacement_to_sep(a_start, a_end, into, a_rad + b_rad) +end + +--collide 2 line segments +function intersect.line_line_collide(a_start, a_end, a_rad, b_start, b_end, b_rad, into) + --segment directions from start points + local a_dir = a_end:vsub(a_start) + local b_dir = b_end:vsub(b_start) + + --detect degenerate cases + local a_degen = a_dir:length_squared() <= COLLIDE_EPS + local b_degen = a_dir:length_squared() <= COLLIDE_EPS + if a_degen and b_degen then + --actually just circles + return intersect.circle_circle_collide(a_start, a_rad, b_start, b_rad, into) + elseif a_degen then + -- a is just circle; annoying, need reversed msv + local collided = intersect.line_circle_collide(b_start, b_end, b_rad, a_start, a_rad, into) + if collided then + collided:smuli(-1) + end + return collided + elseif b_degen then + --b is just circle + return intersect.line_circle_collide(a_start, a_end, a_rad, b_start, b_rad, into) + end + --otherwise we're _actually_ 2 line segs :) + if into == nil then into = vec2:zero() end + + --first, check intersection + + --(c to lua translation of paul bourke's + -- line intersection algorithm) + local dx1 = (a_end.x - a_start.x) + local dx2 = (b_end.x - b_start.x) + local dy1 = (a_end.y - a_start.y) + local dy2 = (b_end.y - b_start.y) + local dxab = (a_start.x - b_start.x) + local dyab = (a_start.y - b_start.y) + + local denom = dy2 * dx1 - dx2 * dy1 + local numera = dx2 * dyab - dy2 * dxab + local numerb = dx1 * dyab - dy1 * dxab + + --check coincident lines + local intersected = "none" + if + math.abs(numera) < COLLIDE_EPS and + math.abs(numerb) < COLLIDE_EPS and + math.abs(denom) < COLLIDE_EPS + then + intersected = "both" + else + --check parallel, non-coincident lines + if math.abs(denom) < COLLIDE_EPS then + intersected = "none" + else + --get interpolants along segments + local mua = numera / denom + local mub = numerb / denom + --intersection outside segment bounds? + local outside_a = mua < 0 or mua > 1 + local outside_b = mub < 0 or mub > 1 + if outside_a and outside_b then + intersected = "none" + elseif outside_a then + intersected = "b" + elseif outside_b then + intersected = "a" + else + intersected = "both" + --collision point = + --[[vec2:xy( + a_start.x + mua * dx1, + a_start.y + mua * dy1, + )]] + end + end + end + if intersected == "both" then + --simply displace along A normal + return into:vset(a_dir):normalisei():smuli(a_rad + b_rad):rot90li() + else + --dumb as a rock check-corners approach + --todo: pool storage + --todo calculus from http://geomalgorithms.com/a07-_distance.html + local search_tab = {} + --only insert corners from the non-intersected line + --since intersected line is potentially the apex + if intersected ~= "a" then + --a endpoints + table.insert(search_tab, {intersect._line_to_point(b_start, b_end, a_start), 1}) + table.insert(search_tab, {intersect._line_to_point(b_start, b_end, a_end), 1}) + end + if intersected ~= "b" then + --b endpoints + table.insert(search_tab, {intersect._line_to_point(a_start, a_end, b_start), -1}) + table.insert(search_tab, {intersect._line_to_point(a_start, a_end, b_end), -1}) + end + + local best = table.find_best(search_tab, function(v) + return -(v[1]:length_squared()) + end) + + --fix direction + into:vset(best[1]):smuli(best[2]) + + return intersect._line_displacement_to_sep(a_start, a_end, into, a_rad + b_rad) + end + + return false +end + +------------------------------------------------------------------------------ +-- axis aligned bounding boxes + +--return true on overlap, false otherwise +local _apo_delta = vec2:zero() +function intersect.aabb_point_overlap(pos, hs, v) + _apo_delta:vset(pos):vsubi(v):absi() + return _apo_delta.x < hs.x and _apo_delta.y < hs.y +end + +--return true on overlap, false otherwise +local _aao_abs_delta = vec2:zero() +local _aao_total_size = vec2:zero() +function intersect.aabb_aabb_overlap(pos, hs, opos, ohs) + _aao_abs_delta:vset(pos):vsubi(opos):absi() + _aao_total_size:vset(hs):vaddi(ohs) + return _aao_abs_delta.x < _aao_total_size.x and _aao_abs_delta.y < _aao_total_size.y +end + +--discrete displacement +--return msv on collision, false otherwise +local _aac_delta = vec2:zero() +local _aac_abs_delta = vec2:zero() +local _aac_size = vec2:zero() +local _aac_abs_amount = vec2:zero() +function intersect.aabb_aabb_collide(apos, ahs, bpos, bhs, into) + if not into then into = vec2:zero() end + _aac_delta:vset(apos):vsubi(bpos) + _aac_abs_delta:vset(_aac_delta):absi() + _aac_size:vset(ahs):vaddi(bhs) + _aac_abs_amount:vset(_aac_size):vsubi(_aac_abs_delta) + if _aac_abs_amount.x > COLLIDE_EPS and _aac_abs_amount.y > COLLIDE_EPS then + --actually collided + if _aac_abs_amount.x <= _aac_abs_amount.y then + --x min + if _aac_delta.x < 0 then + return into:sset(-_aac_abs_amount.x, 0) + else + return into:sset(_aac_abs_amount.x, 0) + end + else + --y min + if _aac_delta.y < 0 then + return into:sset(0, -_aac_abs_amount.y) + else + return into:sset(0, _aac_abs_amount.y) + end + end + end + return false +end + +--return normal and fraction of dt encountered on collision, false otherwise +--TODO: re-pool storage here +function intersect.aabb_aabb_collide_continuous( + a_startpos, a_endpos, ahs, + b_startpos, b_endpos, bhs, + into +) + if not into then into = vec2:zero() end + + --compute delta motion + local _self_delta_motion = a_endpos:vsub(a_startpos) + local _other_delta_motion = b_endpos:vsub(b_startpos) + + --cheap "is this even possible" early-out + do + local _self_half_delta = _self_delta_motion:smul(0.5) + local _self_bounds_pos = _self_half_delta:vadd(a_endpos) + local _self_bounds_hs = _self_half_delta:vadd(ahs) + + local _other_half_delta = _other_delta_motion:smul(0.5) + local _other_bounds_pos = _other_half_delta:vadd(b_endpos) + local _other_bounds_hs = _other_half_delta:vadd(bhs) + + if not body._overlap_raw( + _self_bounds_pos, _self_bounds_hs, + _other_bounds_pos, _other_bounds_hs + ) then + return false + end + end + + --get ccd minkowski box + --this is a relative-space box + local _relative_delta_motion = _self_delta_motion:vsub(_other_delta_motion) + local _minkowski_halfsize = ahs:vadd(bhs) + local _minkowski_pos = b_startpos:vsub(a_startpos) + + --if a line seg from our relative motion hits the minkowski box, we're in luck + --slab raycast is speedy + + --alias + local _rmx = _relative_delta_motion.x + local _rmy = _relative_delta_motion.y + + local _inv_x = math.huge + if _rmx ~= 0 then _inv_x = 1 / _rmx end + local _inv_y = math.huge + if _rmy ~= 0 then _inv_y = 1 / _rmy end + + local _minkowski_tl = _minkowski_pos:vsub(_minkowski_halfsize) + local _minkowski_br = _minkowski_pos:vadd(_minkowski_halfsize) + + --clip x + --get edge t along line + local tx1 = (_minkowski_tl.x) * _inv_x + local tx2 = (_minkowski_br.x) * _inv_x + --clip to existing clip space + local txmin = math.min(tx1, tx2) + local txmax = math.max(tx1, tx2) + --clip y + --get edge t along line + local ty1 = (_minkowski_tl.y) * _inv_y + local ty2 = (_minkowski_br.y) * _inv_y + --clip to existing clip space + local tymin = math.min(ty1, ty2) + local tymax = math.max(ty1, ty2) + + --clip space + local tmin = math.max(0, txmin, tymin) + local tmax = math.min(1, txmax, tymax) + + --still some unclipped? collision! + if tmin <= tmax then + --"was colliding at start" + if tmin == 0 then + --todo: maybe collide at old pos, not new pos + local msv = self:collide(other, into) + if msv then + return msv, tmin + else + return false + end + end + + --delta before colliding + local _self_collide_pre = _self_delta_motion:smul(tmin) + --delta after colliding (to be discarded or projected or whatever) + local _self_collide_post = _self_delta_motion:smul(1.0 - tmin) + --get the collision normal + --(whichever boundary crossed _last_ -> normal) + local _self_collide_normal = vec2:zero() + if txmin > tymin then + _self_collide_normal.x = -math.sign(_self_delta_motion.x) + else + _self_collide_normal.y = -math.sign(_self_delta_motion.y) + end + + --travelling away from normal? + if _self_collide_normal:dot(_self_delta_motion) >= 0 then + return false + end + + --just "slide" projection for now + _self_collide_post:vreji(_self_collide_normal) + + --combine + local _final_delta = _self_collide_pre:vadd(_self_collide_post) + + --construct the target position + local _target_pos = a_startpos:vadd(_final_delta) + + --return delta to target pos + local msv = _target_pos:vsub(a_endpos) + + if math.abs(msv.x) > COLLIDE_EPS or math.abs(msv.y) > COLLIDE_EPS then + into:vset(msv) + return into, tmin + end + end + + return false +end + +return intersect \ No newline at end of file diff --git a/math.lua b/math.lua new file mode 100644 index 0000000..3ad0a1a --- /dev/null +++ b/math.lua @@ -0,0 +1,212 @@ +--[[ + extra mathematical functions +]] + +local bit = require("bit") + +--wrap v around range [lo, hi) +function math.wrap(v, lo, hi) + local range = hi - lo + local relative = v - lo + local relative_wrapped = relative % range + local relative_add = relative_wrapped + range + local final_wrap = relative_add % range + return lo + final_wrap +end + +--clamp v to range [lo, hi] +function math.clamp(v, lo, hi) + return math.max(lo, math.min(v, hi)) +end + +function math.clamp01(v) + return math.clamp(v, 0, 1) +end + +--round v to nearest whole +function math.round(v) + return math.floor(v + 0.5) +end + +--round v to one-in x +-- (eg x = 2, v rounded to increments of 0.5) +function math.to_one_in(v, x) + return math.round(v * x) / x +end + +--round v to a given decimal precision +function math.to_precision(v, decimal_points) + return math.to_one_in(v, math.pow(10, decimal_points)) +end + +--0, 1, -1 sign of a scalar +function math.sign(v) + if v < 0 then return -1 end + if v > 0 then return 1 end + return 0 +end + +--linear interpolation between a and b +function math.lerp(a, b, t) + return a * (1.0 - t) + b * t +end + +--classic smoothstep +--(only "safe" for 0-1 range) +function math.smoothstep(v) + return v * v * (3 - 2 * v) +end + +--classic smootherstep; zero 2nd order derivatives at 0 and 1 +--(only safe for 0-1 range) +function math.smootherstep(v) + return v * v * v * (v * (v * 6 - 15) + 10) +end + +--todo: various other easing curves + +--prime number stuff +local primes_1k = { + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, + 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, + 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, + 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, + 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, + 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, + 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, + 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, + 947, 953, 967, 971, 977, 983, 991, 997, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, + 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, + 1229, 1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, + 1381, 1399, 1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511, + 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597, 1601, 1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657, + 1663, 1667, 1669, 1693, 1697, 1699, 1709, 1721, 1723, 1733, 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789, 1801, 1811, + 1823, 1831, 1847, 1861, 1867, 1871, 1873, 1877, 1879, 1889, 1901, 1907, 1913, 1931, 1933, 1949, 1951, 1973, 1979, 1987, + 1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069, 2081, 2083, 2087, 2089, 2099, 2111, 2113, 2129, + 2131, 2137, 2141, 2143, 2153, 2161, 2179, 2203, 2207, 2213, 2221, 2237, 2239, 2243, 2251, 2267, 2269, 2273, 2281, 2287, + 2293, 2297, 2309, 2311, 2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, 2381, 2383, 2389, 2393, 2399, 2411, 2417, 2423, + 2437, 2441, 2447, 2459, 2467, 2473, 2477, 2503, 2521, 2531, 2539, 2543, 2549, 2551, 2557, 2579, 2591, 2593, 2609, 2617, + 2621, 2633, 2647, 2657, 2659, 2663, 2671, 2677, 2683, 2687, 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, 2731, 2741, + 2749, 2753, 2767, 2777, 2789, 2791, 2797, 2801, 2803, 2819, 2833, 2837, 2843, 2851, 2857, 2861, 2879, 2887, 2897, 2903, + 2909, 2917, 2927, 2939, 2953, 2957, 2963, 2969, 2971, 2999, 3001, 3011, 3019, 3023, 3037, 3041, 3049, 3061, 3067, 3079, + 3083, 3089, 3109, 3119, 3121, 3137, 3163, 3167, 3169, 3181, 3187, 3191, 3203, 3209, 3217, 3221, 3229, 3251, 3253, 3257, + 3259, 3271, 3299, 3301, 3307, 3313, 3319, 3323, 3329, 3331, 3343, 3347, 3359, 3361, 3371, 3373, 3389, 3391, 3407, 3413, + 3433, 3449, 3457, 3461, 3463, 3467, 3469, 3491, 3499, 3511, 3517, 3527, 3529, 3533, 3539, 3541, 3547, 3557, 3559, 3571, + 3581, 3583, 3593, 3607, 3613, 3617, 3623, 3631, 3637, 3643, 3659, 3671, 3673, 3677, 3691, 3697, 3701, 3709, 3719, 3727, + 3733, 3739, 3761, 3767, 3769, 3779, 3793, 3797, 3803, 3821, 3823, 3833, 3847, 3851, 3853, 3863, 3877, 3881, 3889, 3907, + 3911, 3917, 3919, 3923, 3929, 3931, 3943, 3947, 3967, 3989, 4001, 4003, 4007, 4013, 4019, 4021, 4027, 4049, 4051, 4057, + 4073, 4079, 4091, 4093, 4099, 4111, 4127, 4129, 4133, 4139, 4153, 4157, 4159, 4177, 4201, 4211, 4217, 4219, 4229, 4231, + 4241, 4243, 4253, 4259, 4261, 4271, 4273, 4283, 4289, 4297, 4327, 4337, 4339, 4349, 4357, 4363, 4373, 4391, 4397, 4409, + 4421, 4423, 4441, 4447, 4451, 4457, 4463, 4481, 4483, 4493, 4507, 4513, 4517, 4519, 4523, 4547, 4549, 4561, 4567, 4583, + 4591, 4597, 4603, 4621, 4637, 4639, 4643, 4649, 4651, 4657, 4663, 4673, 4679, 4691, 4703, 4721, 4723, 4729, 4733, 4751, + 4759, 4783, 4787, 4789, 4793, 4799, 4801, 4813, 4817, 4831, 4861, 4871, 4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937, + 4943, 4951, 4957, 4967, 4969, 4973, 4987, 4993, 4999, 5003, 5009, 5011, 5021, 5023, 5039, 5051, 5059, 5077, 5081, 5087, + 5099, 5101, 5107, 5113, 5119, 5147, 5153, 5167, 5171, 5179, 5189, 5197, 5209, 5227, 5231, 5233, 5237, 5261, 5273, 5279, + 5281, 5297, 5303, 5309, 5323, 5333, 5347, 5351, 5381, 5387, 5393, 5399, 5407, 5413, 5417, 5419, 5431, 5437, 5441, 5443, + 5449, 5471, 5477, 5479, 5483, 5501, 5503, 5507, 5519, 5521, 5527, 5531, 5557, 5563, 5569, 5573, 5581, 5591, 5623, 5639, + 5641, 5647, 5651, 5653, 5657, 5659, 5669, 5683, 5689, 5693, 5701, 5711, 5717, 5737, 5741, 5743, 5749, 5779, 5783, 5791, + 5801, 5807, 5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857, 5861, 5867, 5869, 5879, 5881, 5897, 5903, 5923, 5927, 5939, + 5953, 5981, 5987, 6007, 6011, 6029, 6037, 6043, 6047, 6053, 6067, 6073, 6079, 6089, 6091, 6101, 6113, 6121, 6131, 6133, + 6143, 6151, 6163, 6173, 6197, 6199, 6203, 6211, 6217, 6221, 6229, 6247, 6257, 6263, 6269, 6271, 6277, 6287, 6299, 6301, + 6311, 6317, 6323, 6329, 6337, 6343, 6353, 6359, 6361, 6367, 6373, 6379, 6389, 6397, 6421, 6427, 6449, 6451, 6469, 6473, + 6481, 6491, 6521, 6529, 6547, 6551, 6553, 6563, 6569, 6571, 6577, 6581, 6599, 6607, 6619, 6637, 6653, 6659, 6661, 6673, + 6679, 6689, 6691, 6701, 6703, 6709, 6719, 6733, 6737, 6761, 6763, 6779, 6781, 6791, 6793, 6803, 6823, 6827, 6829, 6833, + 6841, 6857, 6863, 6869, 6871, 6883, 6899, 6907, 6911, 6917, 6947, 6949, 6959, 6961, 6967, 6971, 6977, 6983, 6991, 6997, + 7001, 7013, 7019, 7027, 7039, 7043, 7057, 7069, 7079, 7103, 7109, 7121, 7127, 7129, 7151, 7159, 7177, 7187, 7193, 7207, + 7211, 7213, 7219, 7229, 7237, 7243, 7247, 7253, 7283, 7297, 7307, 7309, 7321, 7331, 7333, 7349, 7351, 7369, 7393, 7411, + 7417, 7433, 7451, 7457, 7459, 7477, 7481, 7487, 7489, 7499, 7507, 7517, 7523, 7529, 7537, 7541, 7547, 7549, 7559, 7561, + 7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, 7639, 7643, 7649, 7669, 7673, 7681, 7687, 7691, 7699, 7703, 7717, 7723, + 7727, 7741, 7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829, 7841, 7853, 7867, 7873, 7877, 7879, 7883, 7901, 7907, 7919, +} + +local sparse_primes_1k = { + 2, 3, 5, 11, 19, 37, 59, 79, 109, 151, 191, 239, 293, 367, 431, 499, 571, 653, 733, 853, 1009, 1151, 1301, 1481, 1613, + 1783, 1951, 2113, 2137, 2311, 2477, 2687, 2857, 3079, 3323, 3541, 3853, 4211, 4549, 4933, 5101, 5479, 5843, 6247, 6653, + 6689, 7039, 7307, 7559, 7573, 7919, +} + +function math.first_above(v, t) + for _,p in ipairs(t) do + if p > v then + return p + end + end + return t[#t] +end + +function math.next_prime_1k(v) + return math.first_above(v, primes_1k) +end + +function math.next_prime_1k_sparse(v) + return math.first_above(v, sparse_primes_1k) +end + +--color handling stuff +local band, bor = bit.band, bit.bor +local lshift, rshift = bit.lshift, bit.rshift + +function math.colorToRGB(r, g, b) + local br = lshift(band(0xff, r * 255), 16) + local bg = lshift(band(0xff, g * 255), 8) + local bb = lshift(band(0xff, b * 255), 0) + return bor( br, bg, bb ) +end + +function math.colorToARGB(r, g, b, a) + local ba = lshift(band(0xff, a * 255), 24) + local br = lshift(band(0xff, r * 255), 16) + local bg = lshift(band(0xff, g * 255), 8) + local bb = lshift(band(0xff, b * 255), 0) + return bor( br, bg, bb, ba ) +end + +function math.colorToRGBA(r, g, b, a) + local br = lshift(band(0xff, r * 255), 24) + local bg = lshift(band(0xff, g * 255), 16) + local bb = lshift(band(0xff, b * 255), 8) + local ba = lshift(band(0xff, a * 255), 0) + return bor( br, bg, bb, ba ) +end + +function math.ARGBToColor(argb) + local r = rshift(band(argb, 0x00ff0000), 16) / 255.0 + local g = rshift(band(argb, 0x0000ff00), 8) / 255.0 + local b = rshift(band(argb, 0x000000ff), 0) / 255.0 + local a = rshift(band(argb, 0xff000000), 24) / 255.0 + return r, g, b, a +end + +function math.RGBAToColor(rgba) + local r = rshift(band(rgba, 0xff000000), 24) / 255.0 + local g = rshift(band(rgba, 0x00ff0000), 16) / 255.0 + local b = rshift(band(rgba, 0x0000ff00), 8) / 255.0 + local a = rshift(band(rgba, 0x000000ff), 0) / 255.0 + return r, g, b, a +end + +function math.RGBToColor(rgb) + local r = rshift(band(rgb, 0x00ff0000), 16) / 255.0 + local g = rshift(band(rgb, 0x0000ff00), 8) / 255.0 + local b = rshift(band(rgb, 0x000000ff), 0) / 255.0 + local a = 1.0 + return r, g, b, a +end + +--angle handling stuff +function math.normalise_angle(a) + return math.wrap(a, -math.pi, math.pi) +end + +function math.relative_angle(a1, a2) + a1 = math.normalise_angle(a1) + a2 = math.normalise_angle(a2) + return math.normalise_angle(a1 - a2) +end + +--geometric rotation multi-return +function math.rotate(x, y, r) + local s = math.sin(r) + local c = math.cos(r) + return c * x - s * y, s * x + c * y +end diff --git a/oo.lua b/oo.lua new file mode 100644 index 0000000..89491eb --- /dev/null +++ b/oo.lua @@ -0,0 +1,43 @@ +--[[ + barebones oop basics + supports basic inheritance and means you don't have to build/set your own metatable each time + + todo: collect some stats on classes/optional global class registry +]] + +function class(inherits) + local c = {} + c.__mt = {__index = c} + --handle single inheritence + if type(inherits) == "table" and inherits.__mt then + setmetatable(c, inherits.__mt) + end + --common class functions + + --internal initialisation + --sets up an initialised object with a default value table + --performing a super construction if necessary and assigning the right metatable + function c:init(t, ...) + if inherits then + --super ctor, then overlay args table + t = table.overlay(inherits:new(...), t) + end + --upgrade to this class and return + return setmetatable(t, self.__mt) + end + + --constructor + --generally to be overridden + function c:new() + return self:init({}) + end + + --get the inherited class for super calls if/as needed + --allows overrides that still refer to superclass behaviour + function c:super() + return inherits + end + + --done + return c +end diff --git a/stable_sort.lua b/stable_sort.lua new file mode 100644 index 0000000..0f6686b --- /dev/null +++ b/stable_sort.lua @@ -0,0 +1,152 @@ +-- stable sorting routines for lua +-- +-- modifies the global table namespace so you don't have +-- to re-require it everywhere. +-- +-- table.stable_sort +-- a fast stable sort +-- table.unstable_sort +-- alias for the builtin unstable table.sort +-- table.insertion_sort +-- an insertion sort, should you prefer it +-- + +--this is based on MIT licensed code from Dirk Laurie and Steve Fisher +--license as follows: + +--[[ + Copyright © 2013 Dirk Laurie and Steve Fisher. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +]] + +-- (modifications by Max Cahill 2018) + +local _sort_core = {} + +--tunable size for +_sort_core.max_chunk_size = 24 + +function _sort_core.insertion_sort_impl( array, first, last, less ) + for i = first + 1, last do + local k = first + local v = array[i] + for j = i, first + 1, -1 do + if less( v, array[j-1] ) then + array[j] = array[j-1] + else + k = j + break + end + end + array[k] = v + end +end + +function _sort_core.merge( array, workspace, low, middle, high, less ) + local i, j, k + i = 1 + -- copy first half of array to auxiliary array + for j = low, middle do + workspace[ i ] = array[ j ] + i = i + 1 + end + -- sieve through + i = 1 + j = middle + 1 + k = low + while true do + if (k >= j) or (j > high) then + break + end + if less( array[ j ], workspace[ i ] ) then + array[ k ] = array[ j ] + j = j + 1 + else + array[ k ] = workspace[ i ] + i = i + 1 + end + k = k + 1 + end + -- copy back any remaining elements of first half + for k = k, j-1 do + array[ k ] = workspace[ i ] + i = i + 1 + end +end + + +function _sort_core.merge_sort_impl(array, workspace, low, high, less) + if high - low <= _sort_core.max_chunk_size then + _sort_core.insertion_sort_impl( array, low, high, less ) + else + local middle = math.floor((low + high)/2) + _sort_core.merge_sort_impl( array, workspace, low, middle, less ) + _sort_core.merge_sort_impl( array, workspace, middle + 1, high, less ) + _sort_core.merge( array, workspace, low, middle, high, less ) + end +end + +--inline common setup stuff +function _sort_core.sort_setup(array, less) + local n = #array + local trivial = false + --trivial cases; empty or 1 element + if n <= 1 then + trivial = true + else + --default less + less = less or function (a, b) + return a < b + end + --check less + if less(array[1], array[1]) then + error("invalid order function for sorting") + end + end + --setup complete + return trivial, n, less +end + +function _sort_core.stable_sort(array, less) + --setup + local trivial, n, less = _sort_core.sort_setup(array, less) + if not trivial then + --temp storage + local workspace = {} + workspace[ math.floor( (n+1)/2 ) ] = array[1] + --dive in + _sort_core.merge_sort_impl( array, workspace, 1, n, less ) + end + return array +end + +function _sort_core.insertion_sort(array, less) + --setup + local trivial, n, less = _sort_core.sort_setup(array, less) + if not trivial then + _sort_core.insertion_sort_impl(array, 1, n, less) + end + return array +end + +--export sort core +table.insertion_sort = _sort_core.insertion_sort +table.stable_sort = _sort_core.stable_sort +table.unstable_sort = table.sort diff --git a/state_machine.lua b/state_machine.lua new file mode 100644 index 0000000..75feed3 --- /dev/null +++ b/state_machine.lua @@ -0,0 +1,135 @@ +--[[ + state machine + + a finite state machine implementation; + each state is a table with optional enter, exit, update and draw callbacks + which each optionally take the machine, and the state table as arguments + + on changing state, the outgoing state's exit callback is called, then the incoming state's + enter callback is called. + + on update, the current state's update callback is called + on draw, the current state's draw callback is called + + TODO: consider coroutine friendliness + + depends on oo.lua supplying class() +]] + +local state_machine = class() + +function state_machine:new(states, start) + local ret = self:init({ + states = states or {}, + current_state = "" + }) + + if start then + ret:set_state(start) + end + + return ret +end + +------------------------------------------------------------------------------- +--internal helpers + +function state_machine:_get_state() + return self.states[self.current_state] +end + +--make an internal call, with up to 4 arguments +function state_machine:_call(name, a, b, c, d) + local state = self:_get_state() + if state and type(state[name]) == "function" then + return state[name](self, state, a, b, c, d) + end + return nil +end + +------------------------------------------------------------------------------- +--various checks + +function state_machine:in_state(name) + return self.current_state == name +end + +function state_machine:has_state(name) + return self.states[name] ~= nil +end + +------------------------------------------------------------------------------- +--state adding/removing + +--add a state +function state_machine:add_state(name, data) + if self.has_state(name) then + error("error: added duplicate state "..name) + else + self.states[name] = data + if self:in_state(name) then + self:_call("enter") + end + end + + return self +end + +--remove a state +function state_machine:remove_state(name) + if not self.has_state(name) then + error("error: removed missed state "..name) + else + if self:in_state(name) then + self:_call("exit") + end + self.states[name] = nil + end + + return self +end + +--hard-replace a state table +--if do_transitions is truthy and we're replacing the current state, +--exit is called on the old state and enter is called on the new state +function state_machine:replace_state(name, data, do_transitions) + local current = self:in_state(name) + if do_transitions and current then + self:_call("exit") + end + self.states[name] = data + if do_transitions and current then + self:_call("enter") + end + + return self +end + +--ensure a state doesn't exist +function state_machine:clear_state(name) + return self:replace_state(name, nil, true) +end + +------------------------------------------------------------------------------- +--transitions and updates + +function state_machine:set_state(state, reset) + if self.current_state ~= state or reset then + self:_call("exit") + self.current_state = state + self:_call("enter") + end + return self +end + +--perform an update +--pass in an optional delta time which is passed as an arg to the state functions +function state_machine:update(dt) + return self:_call("update", dt) +end + +function state_machine:draw() + self:_call("draw") +end + +return state_machine \ No newline at end of file diff --git a/table.lua b/table.lua new file mode 100644 index 0000000..b8bd91c --- /dev/null +++ b/table.lua @@ -0,0 +1,108 @@ +--[[ + extra table routines +]] + +--return the back element of a table +function table.back(t) + return t[#t] +end + +--remove the back element of a table and return it +function table.pop(t) + return table.remove(t) +end + +--insert to the back of a table +function table.push(t, v) + return table.insert(t, v) +end + +--remove the front element of a table and return it +function table.shift(t) + return table.remove(t, 1) +end + +--insert to the front of a table +function table.unshift(t, v) + return table.insert(t, 1, v) +end + +--find the index in a sequential table that a resides at +--or nil if nothing was found +--(todo: consider pairs version?) +function table.index_of(t, a) + if a == nil then return nil end + for i,b in ipairs(t) do + if a == b then + return i + end + end + return nil +end + +--remove the first instance of value from a table (linear search) +--returns true if the value was removed, else false +function table.remove_value(t, a) + local i = table.index_of(t, a) + if i then + table.remove(t, i) + return true + end + return false +end + +--add a value to a table if it doesn't already exist (linear search) +--returns true if the value was added, else false +function table.add_value(t, a) + local i = table.index_of(t, a) + if not i then + table.insert(t, a) + return true + end + return false +end + +--helper for optionally passed random +local _global_random = love.math.random or math.random +local function _random(min, max, r) + return r and r:random(min, max) + or _global_random(min, max) +end + +--pick a random value from a table (or nil if it's empty) +function table.pick_random(t, r) + if #t == 0 then + return nil + end + return t[_random(1, #t, r)] +end + +--shuffle the order of a table +function table.shuffle(t, r) + for i = 1, #t do + local j = _random(1, #t, r) + t[i], t[j] = t[j], t[i] + end +end + +--(might already exist depending on luajit) +if table.clear == nil then + --destructively clear a numerically keyed table + --useful when multiple references are floating around + --so you cannot just pop a new table out of nowhere + function table.clear(t) + assert(type(to) == "table", "table.overlay - argument 'to' must be a table") + while t[1] ~= nil do + table.remove(t) + end + end +end + +function table.overlay(to, from) + assert(type(to) == "table", "table.overlay - argument 'to' must be a table") + assert(type(from) == "table", "table.overlay - argument 'from' must be a table") + for k,v in pairs(from) do + to[k] = v + end + return to +end diff --git a/vec2.lua b/vec2.lua new file mode 100644 index 0000000..a7b78b7 --- /dev/null +++ b/vec2.lua @@ -0,0 +1,579 @@ +--[[ + 2d vector type +]] + +--[[ + notes: + + depends on a class() function as in oo.lua + + some methods depend on math library extensions + + math.clamp(v, min, max) - return v clamped between min and max + math.round(v) - round v downwards if fractional part is < 0.5 +]] + +local vec2 = class() +vec2.x = 0 +vec2.y = 0 + +--probably-too-flexible ctor +function vec2:new(x, y) + if x and y then + return vec2:xy(x,y) + elseif x then + if type(x) == "number" then + return vec2:filled(x) + elseif x.copy then + return x:copy() + end + end + return vec2:zero() +end + +--explicit ctors +function vec2:copy() + return self:init({ + x = self.x, y = self.y + }) +end + +function vec2:xy(x, y) + return self:init({ + x = x, y = y + }) +end + +function vec2:filled(v) + return self:init({ + x = v, y = v + }) +end + +function vec2:zero() + return vec2:filled(0) +end + +--shared pooled storage +local _vec2_pool = {} +--size limit for tuning memory upper bound +local _vec2_pool_limit = 128 + +function vec2.pool_size() + return #_vec2_pool +end + +--flush the entire pool +function vec2.flush_pool() + if vec2.pool_size() > 0 then + _vec2_pool = {} + end +end + +--drain one element from the pool, if it exists +function vec2.drain_pool() + if #_vec2_pool > 0 then + return table.remove(_vec2_pool) + end + return nil +end + +--get a pooled vector (initialise it yourself) +function vec2:pooled() + return vec2.drain_pool() or vec2:zero() +end + +--get a pooled copy of an existing vector +function vec2:pooled_copy() + return vec2:pooled():vset(self) +end + +--release a vector to the pool +function vec2:release() + if vec2.pool_size() < _vec2_pool_limit then + table.insert(_vec2_pool, self) + end +end + +--unpack for multi-args + +function vec2:unpack() + return self.x, self.y +end + +--pack when a sequence is needed +--(not particularly useful) + +function vec2:pack() + return {self:unpack()} +end + +--modify + +function vec2:sset(x, y) + if not y then y = x end + self.x = x + self.y = y + return self +end + +function vec2:vset(v) + self.x = v.x + self.y = v.y + return self +end + +function vec2:swap(v) + local sx, sy = self.x, self.y + self:vset(v) + v:sset(sx, sy) + return self +end + +----------------------------------------------------------- +--equality comparison +----------------------------------------------------------- + +--threshold for equality in each dimension +local EQUALS_EPSILON = 1e-9 + +--true if a and b are functionally equivalent +function vec2.equals(a, b) + return ( + math.abs(a.x - b.x) <= EQUALS_EPSILON and + math.abs(a.y - b.y) <= EQUALS_EPSILON + ) +end + +--true if a and b are not functionally equivalent +--(very slightly faster than `not vec2.equals(a, b)`) +function vec2.nequals(a, b) + return ( + math.abs(a.x - b.x) > EQUALS_EPSILON or + math.abs(a.y - b.y) > EQUALS_EPSILON + ) +end + +----------------------------------------------------------- +--arithmetic +----------------------------------------------------------- + +--immediate mode + +--vector +function vec2:vaddi(v) + self.x = self.x + v.x + self.y = self.y + v.y + return self +end + +function vec2:vsubi(v) + self.x = self.x - v.x + self.y = self.y - v.y + return self +end + +function vec2:vmuli(v) + self.x = self.x * v.x + self.y = self.y * v.y + return self +end + +function vec2:vdivi(v) + self.x = self.x / v.x + self.y = self.y / v.y + return self +end + +--scalar +function vec2:saddi(x, y) + if y then + self.x = self.x + x + self.y = self.y + y + else + self.x = self.x + x + self.y = self.y + x + end + return self +end + +function vec2:ssubi(x, y) + if y then + self.x = self.x - x + self.y = self.y - y + else + self.x = self.x - x + self.y = self.y - x + end + return self +end + +function vec2:smuli(x, y) + if y then + self.x = self.x * x + self.y = self.y * y + else + self.x = self.x * x + self.y = self.y * x + end + return self +end + +function vec2:sdivi(x, y) + if y then + self.x = self.x / x + self.y = self.y / y + else + self.x = self.x / x + self.y = self.y / x + end + return self +end + +--garbage mode + +function vec2:vadd(v) + return self:copy():vaddi(v) +end + +function vec2:vsub(v) + return self:copy():vsubi(v) +end + +function vec2:vmul(v) + return self:copy():vmuli(v) +end + +function vec2:vdiv(v) + return self:copy():vdivi(v) +end + +function vec2:sadd(x, y) + return self:copy():saddi(x, y) +end + +function vec2:ssub(x, y) + return self:copy():ssubi(x, y) +end + +function vec2:smul(x, y) + return self:copy():smuli(x, y) +end + +function vec2:sdiv(x, y) + return self:copy():sdivi(x, y) +end + +--fused multiply-add (a + (b * t)) + +function vec2:fmai(v, t) + self.x = self.x + (v.x * t) + self.y = self.y + (v.y * t) + return self +end + +function vec2:fma(v, t) + return self:copy():fmai(v, t) +end + +----------------------------------------------------------- +-- geometric methods +----------------------------------------------------------- + +function vec2:length_squared() + return self.x * self.x + self.y * self.y +end + +function vec2:length() + return math.sqrt(self:length_squared()) +end + +function vec2:distance_squared(other) + local dx = self.x - other.x + local dy = self.y - other.y + return dx * dx + dy * dy +end + +function vec2:distance(other) + return math.sqrt(self:distance_squared(other)) +end + +--immediate mode + +function vec2:normalisei_both() + local len = self:length() + if len == 0 then + return self, 0 + end + return self:sdivi(len), len +end + +function vec2:normalisei() + local v, len = self:normalisei_both() + return v +end + +function vec2:normalisei_len() + local v, len = self:normalisei_both() + return len +end + +function vec2:inversei() + return self:smuli(-1) +end + +function vec2:rotatei(angle) + local s = math.sin(angle) + local c = math.cos(angle) + local ox = self.x + local oy = self.y + self.x = c * ox - s * oy + self.y = s * ox + c * oy + return self +end + +function vec2:rot90ri() + local ox = self.x + local oy = self.y + self.x = -oy + self.y = ox + return self +end + +function vec2:rot90li() + local ox = self.x + local oy = self.y + self.x = oy + self.y = -ox + return self +end + +vec2.rot180i = vec2.inversei --alias + +function vec2:rotate_aroundi(angle, pivot) + local s = math.sin(angle) + local c = math.cos(angle) + local ox = self.x - pivot.x + local oy = self.y - pivot.y + self.x = (c * ox - s * oy) + pivot.x + self.y = (s * ox + c * oy) + pivot.y + return self +end + +function vec2:rotate_around(angle, pivot) + return self:copy():rotate_aroundi(angle, pivot) +end + +--garbage mode + +function vec2:normalised() + return self:copy():normalisei() +end + +function vec2:normalised_len() + local v = self:copy() + local len = v:normalisei_len() + return v, len +end + +function vec2:inverse() + return self:copy():inversei() +end + +function vec2:rotate(angle) + return self:copy():rotatei(angle) +end + +function vec2:rot90r() + return self:copy():rot90ri() +end + +function vec2:rot90l() + return self:copy():rot90li() +end + +vec2.rot180 = vec2.inverse --alias + +function vec2:angle() + return math.atan2(self.y, self.x) +end + +----------------------------------------------------------- +-- per-component clamping ops +----------------------------------------------------------- + +function vec2:mini(v) + self.x = math.min(self.x, v.x) + self.y = math.min(self.y, v.y) + return self +end + +function vec2:maxi(v) + self.x = math.max(self.x, v.x) + self.y = math.max(self.y, v.y) + return self +end + +function vec2:clampi(min, max) + self.x = math.clamp(self.x, min.x, max.x) + self.y = math.clamp(self.y, min.y, max.y) + return self +end + +function vec2:min(v) + return self:copy():mini(v) +end + +function vec2:max(v) + return self:copy():maxi(v) +end + +function vec2:clamp(min, max) + return self:copy():clampi(min, max) +end + +----------------------------------------------------------- +-- absolute value +----------------------------------------------------------- + +function vec2:absi() + self.x = math.abs(self.x) + self.y = math.abs(self.y) + return self +end + +function vec2:abs() + return self:copy():absi() +end + +----------------------------------------------------------- +-- truncation/rounding +----------------------------------------------------------- + +function vec2:floori() + self.x = math.floor(self.x) + self.y = math.floor(self.y) + return self +end + +function vec2:ceili() + self.x = math.ceil(self.x) + self.y = math.ceil(self.y) + return self +end + +function vec2:roundi() + self.x = math.round(self.x) + self.y = math.round(self.y) + return self +end + +function vec2:floor() + return self:copy():floori() +end + +function vec2:ceil() + return self:copy():ceili() +end + +function vec2:round() + return self:copy():roundi() +end + +----------------------------------------------------------- +-- interpolation +----------------------------------------------------------- + +function vec2:lerpi(other, amount) + self.x = math.lerp(self.x, other.x, amount) + self.y = math.lerp(self.y, other.y, amount) + return self +end + +function vec2:lerp(other, amount) + return self:copy():lerpi(other, amount) +end + +----------------------------------------------------------- +-- vector products and projections +----------------------------------------------------------- + +function vec2.dot(a, b) + return a.x * b.x + a.y * b.y +end + +--"fake", but useful - also called the wedge product apparently +function vec2.cross(a, b) + return a.x * b.y - a.y * b.x +end + +--scalar projection a onto b +function vec2.sproj(a, b) + local len = b:length() + if len == 0 then + return 0 + end + return a:dot(b) / len +end + +--vector projection a onto b (writes into a) +function vec2.vproji(a, b) + local div = b:dot(b) + if div == 0 then + return a:sset(0,0) + end + local fac = a:dot(b) / div + return a:vset(b):smuli(fac) +end + +function vec2.vproj(a, b) + return a:copy():vproji(b) +end + +--vector rejection a onto b (writes into a) +function vec2.vreji(a, b) + local tx, ty = a.x, a.y + a:vproji(b) + a:sset(tx - a.x, ty - a.y) + return a +end + +function vec2.vrej(a, b) + return a:copy():vreji(b) +end + +----------------------------------------------------------- +-- vector extension methods for special purposes +-- (any common vector ops worth naming) +----------------------------------------------------------- + +--"physical" friction +local _v_friction = vec2:zero() --avoid alloc +function vec2:apply_friction(mu, dt) + _v_friction:vset(self):smuli(mu * dt) + if _v_friction:length_squared() > self:length_squared() then + self:sset(0, 0) + else + self:vsubi(_v_friction) + end + return self +end + +--"gamey" friction in one dimension +function apply_friction_1d(v, mu, dt) + local friction = mu * v * dt + if math.abs(friction) > math.abs(v) then + return 0 + else + return v - friction + end +end + +--"gamey" friction in both dimensions +function vec2:apply_friction_xy(mu_x, mu_y, dt) + self.x = apply_friction_1d(self.x, mu_x, dt) + self.y = apply_friction_1d(self.y, mu_y, dt) + return self +end + +return vec2