diff --git a/functional.lua b/functional.lua index 6f077ca..801868c 100644 --- a/functional.lua +++ b/functional.lua @@ -13,6 +13,7 @@ local path = (...):gsub("functional", "") local tablex = require(path .. "tablex") +local mathx = require(path .. "mathx") local functional = setmetatable({}, { __index = tablex, @@ -172,6 +173,54 @@ function functional.zip(t1, t2, f) return ret end +----------------------------------------------------------- +--specialised maps +-- (experimental: let me know if you have better names for these!) +----------------------------------------------------------- + +--maps a sequence {a, b, c} -> collapse { f(a), f(b), f(c) } +-- (ie results from functions should generally be sequences, +-- which are appended onto each other, resulting in one big sequence) +-- (automatically drops any nils, same as map) +function functional.stitch(t, f) + local result = {} + for i, v in ipairs(t) do + local v = f(v, i) + if v ~= nil then + if type(v) == "table" then + for _, e in ipairs(v) do + table.insert(result, e) + end + else + table.insert(result, v) + end + end + end + return result +end + +--alias +functional.map_stitch = functional.stitch + +--maps a sequence {a, b, c} -> { f(a, b), f(b, c), f(c, a) } +-- useful for inter-dependent data +-- (automatically drops any nils, same as map) + +function functional.cycle(t, f) + local result = {} + for i, a in ipairs(t) do + local b = t[mathx.wrap(i + 1, 1, #t + 1)] + local v = f(a, b, i) + if v ~= nil then + table.insert(result, v) + end + end + return result +end + +functional.map_cycle = functional.cycle + + ----------------------------------------------------------- --generating data ----------------------------------------------------------- diff --git a/intersect.lua b/intersect.lua index b661e70..5740734 100644 --- a/intersect.lua +++ b/intersect.lua @@ -55,7 +55,7 @@ function intersect.circle_circle_collide(a_pos, a_rad, b_pos, b_rad, into) into = vec2:zero() end --normalise, scale to separating distance - into:vset(_ccc_delta):sdiv(dist):smuli(rad - dist) + into:vset(_ccc_delta):sdivi(dist):smuli(rad - dist) return into end return false @@ -97,7 +97,7 @@ function intersect._line_displacement_to_sep(a_start, a_end, separation, total_r 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() + separation:vset(a_end):vsubi(a_start):normalisei():rot90li() else separation:smuli(-sep) end @@ -418,4 +418,60 @@ function intersect.point_in_poly(point, poly) return wn ~= 0 end +--resolution helpers + +--resolve a collision between two bodies, given a (minimum) separating vector +-- from a's frame of reference, like the result of any of the _collide functions +--requires the two positions of the bodies, the msv, and a balance factor +--balance should be between 1 and 0; +-- 1 is only a_pos moving to resolve +-- 0 is only b_pos moving to resolve +-- 0.5 is balanced between both (default) +--note: this wont work as-is for line segments, which have two separate position coordinates +-- you will need to understand what is going on and move the second coordinate yourself +function intersect.resolve_msv(a_pos, b_pos, msv, balance) + balance = balance or 0.5 + a_pos:fmai(msv, balance) + b_pos:fmai(msv, -(1 - balance)) +end + +--bounce a velocity off of a normal (modifying velocity) +--essentially flips the part of the velocity in the direction of the normal +function intersect.bounce_off(velocity, normal, conservation) + --(default) + conservation = conservation or 1 + --take a copy, we need it + local old_vel = vec2.pooled_copy(velocity) + --reject on the normal (keep velocity tangential to the normal) + velocity:vreji(normal) + --add back the complement of the difference; + --basically "flip" the velocity in line with the normal. + velocity:fmai(old_vel:vsubi(velocity), -conservation) + --clean up + old_vel:release() + return velocity +end + +--mutual bounce; two similar bodies bounce off each other, transferring energy +function intersect.mutual_bounce(velocity_a, velocity_b, normal, conservation) + --(default) + conservation = conservation or 1 + --take copies, we need them + local old_a_vel = vec2.pooled_copy(velocity_a) + local old_b_vel = vec2.pooled_copy(velocity_b) + --reject on the normal + velocity_a:vreji(normal) + velocity_b:vreji(normal) + --calculate the amount remaining from the old velocity + --(transfer ownership) + local a_remaining = old_a_vel:vsubi(velocity_a) + local b_remaining = old_b_vel:vsubi(velocity_b) + --transfer it to the other body + velocity_a:fmai(b_remaining, conservation) + velocity_b:fmai(a_remaining, conservation) + --clean up + a_remaining:release() + b_remaining:release() +end + return intersect diff --git a/sequence.lua b/sequence.lua index 4dcb722..6964e83 100644 --- a/sequence.lua +++ b/sequence.lua @@ -24,73 +24,85 @@ sequence.sort = stable_sort --patch various interfaces in a type-preserving way, for method chaining --import copying tablex -function sequence:keys() - return sequence(table.keys(self)) +--(common case where something returns another sequence for chaining) +for _, v in ipairs({ + "keys", + "values", + "dedupe", + "collapse", + "append", + "overlay", + "copy", +}) do + local table_f = table[v] + sequence[v] = function(self, ...) + return sequence(table_f(self, ...)) + end end -function sequence:values() - return sequence(table.values(self)) +--aliases +for _, v in ipairs({ + {"flatten", "collapse"}, +}) do + sequence[v[1]] = sequence[v[2]] end -function sequence:dedupe() - return sequence(table.dedupe(self)) +--import functional interface in method form + +--(common case where something returns another sequence for chaining) +for _, v in ipairs({ + "map", + "map_inplace", + "filter", + "filter_inplace", + "remove_if", + "zip", + "stitch", + "cycle", +}) do + local functional_f = functional[v] + sequence[v] = function(self, ...) + return sequence(functional_f(self, ...)) + end end -function sequence:collapse() - return sequence(table.collapse(self)) -end -sequence.flatten = sequence.collapse - -function sequence:append(...) - return sequence(table.append(self, ...)) +--(cases where we don't want to construct a new sequence) +for _, v in ipairs({ + "foreach", + "reduce", + "any", + "none", + "all", + "count", + "contains", + "sum", + "mean", + "minmax", + "max", + "min", + "find_min", + "find_max", + "find_nearest", + "find_match", +}) do + sequence[v] = functional[v] end -function sequence:overlay(...) - return sequence(table.overlay(self, ...)) -end - -function sequence:copy(...) - return sequence(table.copy(self, ...)) -end - ---import functional interface -function sequence:foreach(f) - return functional.foreach(self, f) -end - -function sequence:reduce(seed, f) - return functional.reduce(self, seed, f) -end - -function sequence:map(f) - return sequence(functional.map(self, f)) -end - -function sequence:map_inplace(f) - return sequence(functional.map_inplace(self, f)) -end - -sequence.remap = sequence.map_inplace - -function sequence:filter(f) - return sequence(functional.filter(self, f)) -end - -function sequence:filter_inplace(f) - return sequence(functional.filter_inplace(self, f)) -end - -function sequence:remove_if(f) - return sequence(functional.remove_if(self, f)) + +--aliases +for _, v in ipairs({ + {"remap", "map_inplace"}, + {"map_stitch", "stitch"}, + {"map_cycle", "cycle"}, + {"find_best", "find_max"}, +}) do + sequence[v[1]] = sequence[v[2]] end +--(anything that needs bespoke wrapping) function sequence:partition(f) local a, b = functional.partition(self, f) return sequence(a), sequence(b) end -function sequence:zip(other, f) - return sequence(functional.zip(self, other, f)) -end - return sequence diff --git a/stringx.lua b/stringx.lua index b0568ab..10e189f 100644 --- a/stringx.lua +++ b/stringx.lua @@ -215,11 +215,8 @@ function stringx.trim(s) return s:sub(head, tail) end +--trim the start of a string function stringx.ltrim(s) - if s == "" or s == string.rep(" ", s:len()) then - return "" - end - local head = 1 for i = 1, #s do if not _whitespace_bytes[s:byte(i)] then @@ -227,13 +224,14 @@ function stringx.ltrim(s) break end end + if head == 1 then + return s + end return s:sub(head) end +--trim the end of a string function stringx.rtrim(s) - if s == "" or s == string.rep(" ", s:len()) then - return "" - end local tail = #s for i = #s, 1, -1 do @@ -243,6 +241,10 @@ function stringx.rtrim(s) end end + if tail == #s then + return s + end + return s:sub(1, tail) end @@ -322,13 +324,13 @@ function stringx.starts_with(s, prefix) return true end -function stringx.ends_with(s, posfix) - if posfix == "" then return true end - - if #posfix > #s then return false end - - for i = 0, #posfix-1 do - if s:byte(#s-i) ~= posfix:byte(#posfix-i) then +--check if a given string ends with another +--(without garbage) +function stringx.ends_with(s, suffix) + local len = #s + local suffix_len = #suffix + for i = 0, suffix_len - 1 do + if s:byte(len - i) ~= suffix:byte(suffix_len - i) then return false end end diff --git a/vec2.lua b/vec2.lua index 31657ac..7769742 100644 --- a/vec2.lua +++ b/vec2.lua @@ -16,7 +16,7 @@ end --probably-too-flexible ctor function vec2:new(x, y) - if x and y then + if type(x) == "number" and type(y) == "number" then return vec2:xy(x,y) elseif x then if type(x) == "number" then