diff --git a/init.lua b/init.lua index 410b612..6819aca 100644 --- a/init.lua +++ b/init.lua @@ -95,7 +95,7 @@ function _batteries:camelCase() end --any acronyms to fully capitalise to avoid "Rgb" and the like - local acronyms = _batteries.set{"rgb", "rgba", "argb", "hsl", "xy", "gc",} + local acronyms = _batteries.set{"rgb", "rgba", "argb", "hsl", "xy", "gc", "aabb",} local function caps_acronym(s) if acronyms:has(s) then s = s:upper() diff --git a/intersect.lua b/intersect.lua index b67dcb9..48c09d1 100644 --- a/intersect.lua +++ b/intersect.lua @@ -38,7 +38,9 @@ end function intersect.circle_circle_collide(a_pos, a_rad, b_pos, b_rad, into) --get delta - local delta = a_pos:pooled_copy():vsubi(b_pos) + local delta = a_pos + :pooled_copy() + :vector_sub_inplace(b_pos) --squared threshold local rad = a_rad + b_rad local dist = delta:length_squared() @@ -47,63 +49,77 @@ function intersect.circle_circle_collide(a_pos, a_rad, b_pos, b_rad, into) if dist == 0 then --singular case; just resolve vertically dist = 1 - delta:sset(0,1) + delta:scalar_set(0, 1) else --get actual distance dist = math.sqrt(dist) end --allocate if needed if into == nil then - into = vec2:zero() + into = vec2(0) end --normalise, scale to separating distance - res = into:vset(delta):sdivi(dist):smuli(rad - dist) + res = into:set(delta) + :scalar_div_inplace(dist) + :scalar_mul_inplace(rad - dist) end delta:release() return res end +function intersect.circle_point_collide(a_pos, a_rad, b, into) + return intersect.circle_circle_collide(a_pos, a_rad, b, 0, into) +end + ------------------------------------------------------------------------------ -- line segments -- todo: separate double-sided, one-sided, and pull-through (along normal) collisions? --get the nearest point on the line segment a from point b function intersect.nearest_point_on_line(a_start, a_end, b_pos, into) - if into == nil then into = vec2:zero() end + if into == nil then into = vec2(0) end --direction of segment - local segment = a_end:pooled_copy():vsubi(a_start) + local segment = a_end:pooled_copy() + :vector_sub_inplace(a_start) --detect degenerate case local lensq = segment:length_squared() if lensq <= COLLIDE_EPS then - into:vset(a_start) + into:set(a_start) else --solve for factor along segment - local point_to_start = b_pos:pooled_copy():vsubi(a_start) + local point_to_start = b_pos:pooled_copy() + :vector_sub_inplace(a_start) local factor = math.clamp01(point_to_start:dot(segment) / lensq) point_to_start:release() - into:vset(segment):smuli(factor):vaddi(a_start) + into:set(segment) + :scalar_mul_inplace(factor) + :vector_add_inplace(a_start) end segment:release() return into end --internal ---vector from line seg to point +--vector from line seg origin to point function intersect._line_to_point(a_start, a_end, b_pos, into) - return intersect.nearest_point_on_line(a_start, a_end, b_pos, into):vsubi(b_pos) + return intersect.nearest_point_on_line(a_start, a_end, b_pos, into) + :vector_sub_inplace(b_pos) end --internal --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 distance = separation:normalise_len_inplace() 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):vsubi(a_start):normalisei():rot90li() + separation:set(a_end) + :vector_sub_inplace(a_start) + :normalise_inplace() + :rot90l_inplace() else - separation:smuli(-sep) + separation:scalar_mul_inplace(-sep) end return separation end @@ -127,28 +143,31 @@ 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) + local a_dir = a_end + :pooled_copy() + :vector_sub_inplace(a_start) + local b_dir = b_end + :pooled_copy() + :vector_sub_inplace(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) + if a_degen or b_degen then + vec2.release(a_dir, b_dir) + 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 + return intersect.circle_line_collide(a_start, a_rad, b_start, b_end, b_rad, into) + 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 - 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 + if into == nil then into = vec2(0) end --first, check intersection @@ -195,43 +214,50 @@ function intersect.line_line_collide(a_start, a_end, a_rad, b_start, b_end, b_ra 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 = nil - local best_len = nil - for _, v in ipairs(search_tab) do - local len = v[1]:length_squared() - if not best_len or len < best_len then - best = v - end - 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) + into:set(a_dir) + vec2.release(a_dir, b_dir) + return into + :normalise_inplace() + :scalar_mul_inplace(a_rad + b_rad) + :rot90l_inplace() end - return false + vec2.release(a_dir, b_dir) + + --dumb as a rock check-corners approach + --todo: pool storage + --todo proper 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 = nil + local best_len = nil + for _, v in ipairs(search_tab) do + local len = v[1]:length_squared() + if not best_len or len < best_len then + best = v + end + end + + --fix direction + into:set(best[1]) + :scalar_mul_inplace(best[2]) + + return intersect._line_displacement_to_sep(a_start, a_end, into, a_rad + b_rad) end ------------------------------------------------------------------------------ @@ -239,7 +265,10 @@ end --return true on overlap, false otherwise function intersect.aabb_point_overlap(pos, hs, v) - local delta = pos:pooled_copy():vsubi(v):absi() + local delta = pos + :pooled_copy() + :vector_sub_inplace(v) + :abs_inplace() local overlap = delta.x < hs.x and delta.y < hs.y delta:release() return overlap @@ -249,21 +278,25 @@ end -- return msv to push point to closest edge of aabb function intersect.aabb_point_collide(pos, hs, v, into) --separation between centres - local delta_c = v:pooled_copy():vsubi(pos) + local delta_c = v + :pooled_copy() + :vector_sub_inplace(pos) --absolute separation - local delta_c_abs = delta_c:pooled_copy():absi() + local delta_c_abs = delta_c + :pooled_copy() + :abs_inplace() local res = false if delta_c_abs.x < hs.x and delta_c_abs.y < hs.y then - res = (into or vec2:zero()) + res = (into or vec2(0)) --separating offset in both directions - :vset(hs) - :vsubi(delta_c_abs) + :set(hs) + :vector_sub_inplace(delta_c_abs) --minimum separating distance - :minori() + :minor_inplace() --in the right direction - :vmuli(delta_c:signi()) + :vector_mul_inplace(delta_c:sign_inplace()) --from the aabb's point of view - :smuli(-1) + :inverse_inplace() end vec2.release(delta_c, delta_c_abs) return res @@ -271,8 +304,13 @@ end --return true on overlap, false otherwise function intersect.aabb_aabb_overlap(a_pos, a_hs, b_pos, b_hs) - local delta = a_pos:pooled_copy():vsubi(b_pos):absi() - local total_size = a_hs:pooled_copy():vaddi(b_hs) + local delta = a_pos + :pooled_copy() + :vector_sub_inplace(b_pos) + :abs_inplace() + local total_size = a_hs + :pooled_copy() + :vector_add_inplace(b_hs) local overlap = delta.x < total_size.x and delta.y < total_size.y vec2.release(delta, total_size) return overlap @@ -281,20 +319,28 @@ end --discrete displacement --return msv on collision, false otherwise function intersect.aabb_aabb_collide(a_pos, a_hs, b_pos, b_hs, into) - if not into then into = vec2:zero() end - local delta = a_pos:pooled_copy():vsubi(b_pos) - local abs_delta = delta:pooled_copy():absi() - local size = a_hs:pooled_copy():vaddi(b_hs) - local abs_amount = size:pooled_copy():vsubi(abs_delta) + local delta = a_pos + :pooled_copy() + :vector_sub_inplace(b_pos) + local abs_delta = delta + :pooled_copy() + :abs_inplace() + local size = a_hs + :pooled_copy() + :vector_add_inplace(b_hs) + local abs_amount = size + :pooled_copy() + :vector_sub_inplace(abs_delta) local res = false if abs_amount.x > COLLIDE_EPS and abs_amount.y > COLLIDE_EPS then + if not into then into = vec2(0) end --actually collided if abs_amount.x <= abs_amount.y then --x min - res = into:sset(abs_amount.x * math.sign(delta.x), 0) + res = into:scalar_set(abs_amount.x * math.sign(delta.x), 0) else --y min - res = into:sset(0, abs_amount.y * math.sign(delta.y)) + res = into:scalar_set(0, abs_amount.y * math.sign(delta.y)) end end return res @@ -302,10 +348,15 @@ end -- helper function to clamp point to aabb function intersect.aabb_point_clamp(pos, hs, v, into) - local v_min = pos:pooled_copy():vsubi(hs) - local v_max = pos:pooled_copy():vaddi(hs) - into = into or vec2:zero() - into:vset(v):clampi(v_min, v_max) + local v_min = pos + :pooled_copy() + :vector_sub_inplace(hs) + local v_max = pos + :pooled_copy() + :vector_add_inplace(hs) + into = into or vec2(0) + into:set(v) + :clamp_inplace(v_min, v_max) vec2.release(v_min, v_max) return into end @@ -320,7 +371,10 @@ end -- return msv on collision, false otherwise function intersect.aabb_circle_collide(a_pos, a_hs, b_pos, b_rad, into) - local abs_delta = a_pos:pooled_copy():vsub(b_pos):absi() + local abs_delta = a_pos + :pooled_copy() + :vector_sub_inplace(b_pos) + :abs_inplace() --circle centre within aabb-like bounds, collide as an aabb local like_aabb = abs_delta.x < a_hs.x or abs_delta.y < a_hs.y --(clean up) @@ -328,7 +382,7 @@ function intersect.aabb_circle_collide(a_pos, a_hs, b_pos, b_rad, into) -- local result if like_aabb then - local pretend_hs = vec2:pooled():sset() + local pretend_hs = vec2:pooled(0, 0) result = intersect.aabb_aabb_collide(a_pos, a_hs, b_pos, pretend_hs, into) pretend_hs:release() else @@ -349,11 +403,17 @@ function intersect.point_in_poly(point, poly) for i, a in ipairs(poly) do local b = poly[i + 1] or poly[1] if a.y <= point.y then - if b.y > point.y and vec2.winding_side(a, b, point) > 0 then + if + b.y > point.y + and vec2.winding_side(a, b, point) > 0 + then wn = wn + 1 end else - if b.y <= point.y and vec2.winding_side(a, b, point) < 0 then + if + b.y <= point.y + and vec2.winding_side(a, b, point) < 0 + then wn = wn - 1 end end @@ -361,6 +421,49 @@ function intersect.point_in_poly(point, poly) return wn ~= 0 end +--reversed versions +--it's annoying to need to flip the order of operands depending on what +--shapes you're working with +--so these functions provide the + +--todo: ensure this is all of them + +--(helper for reversing only if there's actually a vector, preserving false) +function intersect.reverse_msv(result) + if result then + result:inverse_inplace() + end + return result +end + +function intersect.point_circle_overlap(a, b_pos, b_rad) + return intersect.circle_point_overlap(b_pos, b_rad, a) +end + +function intersect.point_circle_collide(a, b_pos, b_rad, into) + return intersect.reverse_msv(intersect.circle_circle_collide(b_pos, b_rad, a, 0, into)) +end + +function intersect.point_aabb_overlap(a, b_pos, b_hs) + return intersect.aabb_point_overlap(b_pos, b_hs, a) +end + +function intersect.point_aabb_collide(a, b_pos, b_hs, into) + return intersect.reverse_msv(intersect.aabb_point_collide(b_pos, b_rad, a, into)) +end + +function intersect.circle_aabb_overlap(a, a_rad, b_pos, b_hs) + return intersect.aabb_circle_overlap(b_pos, b_rad, a, a_rad) +end + +function intersect.circle_aabb_collide(a, a_rad, b_pos, b_hs, into) + return intersect.reverse_msv(intersect.aabb_circle_collide(b_pos, b_rad, a, a_rad, into)) +end + +function intersect.circle_line_collide(a, a_rad, b_start, b_end, b_rad) + return intersect.reverse_msv(intersect.line_circle_collide(b_start, b_end, b_rad, a, a_rad, into)) +end + --resolution helpers --resolve a collision between two bodies, given a (minimum) separating vector @@ -371,11 +474,11 @@ end -- 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 +-- you will need to understand what is going on and move the both coordinates 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)) + a_pos:fused_multiply_add_inplace(msv, balance) + b_pos:fused_multiply_add_inplace(msv, -(1 - balance)) end -- gets a normalised balance factor from two mass inputs, and treats <=0 or infinite or nil masses as static bodies @@ -403,12 +506,12 @@ 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) + local old_vel = velocity:pooled_copy() --reject on the normal (keep velocity tangential to the normal) - velocity:vreji(normal) + velocity:vector_rejection_inplace(normal) --add back the complement of the difference; --basically "flip" the velocity in line with the normal. - velocity:fmai(old_vel:vsubi(velocity), -conservation) + velocity:fused_multiply_add_inplace(old_vel:vector_sub_inplace(velocity), -conservation) --clean up old_vel:release() return velocity @@ -419,18 +522,18 @@ 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) + local old_a_vel = velocity_a:pooled_copy() + local old_b_vel = velocity_b:pooled_copy() --reject on the normal - velocity_a:vreji(normal) - velocity_b:vreji(normal) + velocity_a:vector_rejection_inplace(normal) + velocity_b:vector_rejection_inplace(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 pool ownership) + local a_remaining = old_a_vel:vector_sub_inplace(velocity_a) + local b_remaining = old_b_vel:vector_sub_inplace(velocity_b) --transfer it to the other body - velocity_a:fmai(b_remaining, conservation) - velocity_b:fmai(a_remaining, conservation) + velocity_a:fused_multiply_add_inplace(b_remaining, conservation) + velocity_b:fused_multiply_add_inplace(a_remaining, conservation) --clean up vec2.release(a_remaining, b_remaining) end diff --git a/vec2.lua b/vec2.lua index e712228..32656ee 100644 --- a/vec2.lua +++ b/vec2.lua @@ -16,22 +16,24 @@ function vec2:__tostring() return ("(%.2f, %.2f)"):format(self.x, self.y) end ---probably-too-flexible ctor +--ctor function vec2:new(x, y) - if type(x) == "number" or type(x) == "nil" then - self:sset(x or 0, y) + if type(x) == "number" then + self:scalar_set(x, y) elseif type(x) == "table" then if type(x.type) == "function" and x:type() == "vec2" then - self:vset(x) + self:vector_set(x) elseif x[1] then - self:sset(x[1], x[2]) + self:scalar_set(x[1], x[2]) else - self:sset(x.x, x.y) + self:scalar_set(x.x, x.y) end + else + self:scalar_set(0) end end ---explicit ctors +--explicit ctors; mostly vestigial at this point function vec2:copy() return vec2(self.x, self.y) end @@ -40,8 +42,12 @@ function vec2:xy(x, y) return vec2(x, y) end +function vec2:polar(length, angle) + return vec2(length, 0):rotate_inplace(angle) +end + function vec2:filled(v) - return vec2(v) + return vec2(v, v) end function vec2:zero() @@ -63,28 +69,28 @@ make_pooled(vec2, 128) --get a pooled copy of an existing vector function vec2:pooled_copy() - return vec2:pooled():vset(self) + return vec2:pooled(self) end --modify -function vec2:sset(x, y) +function vec2:vector_set(v) + self.x = v.x + self.y = v.y + return self +end + +function vec2:scalar_set(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) + self.x, self.y = v.x, v.y + v.x, v.y = sx, sy return self end @@ -116,108 +122,68 @@ end --arithmetic ----------------------------------------------------------- ---immediate mode - --vector -function vec2:vaddi(v) +function vec2:vector_add_inplace(v) self.x = self.x + v.x self.y = self.y + v.y return self end -function vec2:vsubi(v) +function vec2:vector_sub_inplace(v) self.x = self.x - v.x self.y = self.y - v.y return self end -function vec2:vmuli(v) +function vec2:vector_mul_inplace(v) self.x = self.x * v.x self.y = self.y * v.y return self end -function vec2:vdivi(v) +function vec2:vector_div_inplace(v) self.x = self.x / v.x self.y = self.y / v.y return self end +--(a + (b * t)) +--useful for integrating physics and adding directional offsets +function vec2:fused_multiply_add_inplace(v, t) + self.x = self.x + (v.x * t) + self.y = self.y + (v.y * t) + return self +end + --scalar -function vec2:saddi(x, y) +function vec2:scalar_add_inplace(x, y) if not y then y = x end self.x = self.x + x self.y = self.y + y return self end -function vec2:ssubi(x, y) +function vec2:scalar_sub_inplace(x, y) if not y then y = x end self.x = self.x - x self.y = self.y - y return self end -function vec2:smuli(x, y) +function vec2:scalar_mul_inplace(x, y) if not y then y = x end self.x = self.x * x self.y = self.y * y return self end -function vec2:sdivi(x, y) +function vec2:scalar_div_inplace(x, y) if not y then y = x end self.x = self.x / x self.y = self.y / y 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 ----------------------------------------------------------- @@ -240,31 +206,31 @@ function vec2:distance(other) return math.sqrt(self:distance_squared(other)) end ---immediate mode - -function vec2:normalisei_both() +function vec2:normalise_both_inplace() local len = self:length() if len == 0 then return self, 0 end - return self:sdivi(len), len + return self:scalar_div_inplace(len), len end -function vec2:normalisei() - local v, len = self:normalisei_both() +function vec2:normalise_inplace() + local v, len = self:normalise_both_inplace() return v end -function vec2:normalisei_len() - local v, len = self:normalisei_both() +function vec2:normalise_len_inplace() + local v, len = self:normalise_both_inplace() return len end -function vec2:inversei() - return self:smuli(-1) +function vec2:inverse_inplace() + return self:scalar_mul_inplace(-1) end -function vec2:rotatei(angle) +-- angle/direction specific + +function vec2:rotate_inplace(angle) local s = math.sin(angle) local c = math.cos(angle) local ox = self.x @@ -274,7 +240,15 @@ function vec2:rotatei(angle) return self end -function vec2:rot90ri() +function vec2:rotate_around_inplace(angle, pivot) + self:vector_sub_inplace(pivot) + self:rotate_inplace(angle) + self:vector_add_inplace(pivot) + return self +end + +--fast quarter/half rotations +function vec2:rot90r_inplace() local ox = self.x local oy = self.y self.x = -oy @@ -282,7 +256,7 @@ function vec2:rot90ri() return self end -function vec2:rot90li() +function vec2:rot90l_inplace() local ox = self.x local oy = self.y self.x = oy @@ -290,50 +264,7 @@ function vec2:rot90li() return self end -vec2.rot180i = vec2.inversei --alias - -function vec2:rotate_aroundi(angle, pivot) - self:vsubi(pivot) - self:rotatei(angle) - self:vaddi(pivot) - return self -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:rotate_around(angle, pivot) - return self:copy():rotate_aroundi(angle, pivot) -end - --- angle/direction specific +vec2.rot180_inplace = vec2.inverse_inplace --alias --get the angle of this vector relative to (1, 0) function vec2:angle() @@ -347,180 +278,125 @@ end --lerp towards the direction of a provided vector --(length unchanged) -function vec2:lerp_directioni(v, t) - return self:rotatei(self:angle_difference(v) * t) -end - -function vec2:lerp_direction(v, t) - return self:copy():lerp_directioni(v, t) +function vec2:lerp_direction_inplace(v, t) + return self:rotate_inplace(self:angle_difference(v) * t) end ----------------------------------------------------------- -- per-component clamping ops ----------------------------------------------------------- -function vec2:mini(v) +function vec2:min_inplace(v) self.x = math.min(self.x, v.x) self.y = math.min(self.y, v.y) return self end -function vec2:maxi(v) +function vec2:max_inplace(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) +function vec2:clamp_inplace(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() +function vec2:abs_inplace() self.x = math.abs(self.x) self.y = math.abs(self.y) return self end -function vec2:abs() - return self:copy():absi() -end - ----------------------------------------------------------- -- sign ----------------------------------------------------------- -function vec2:signi() +function vec2:sign_inplace() self.x = math.sign(self.x) self.y = math.sign(self.y) return self end -function vec2:sign() - return self:copy():signi() -end - ----------------------------------------------------------- -- truncation/rounding ----------------------------------------------------------- -function vec2:floori() +function vec2:floor_inplace() self.x = math.floor(self.x) self.y = math.floor(self.y) return self end -function vec2:ceili() +function vec2:ceil_inplace() self.x = math.ceil(self.x) self.y = math.ceil(self.y) return self end -function vec2:roundi() +function vec2:round_inplace() 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) +function vec2:lerp_inplace(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 - -function vec2:lerp_epsi(other, amount, eps) +function vec2:lerp_eps_inplace(other, amount, eps) self.x = math.lerp_eps(self.x, other.x, amount, eps) self.y = math.lerp_eps(self.y, other.y, amount, eps) return self end -function vec2:lerp_eps(other, amount, eps) - return self:copy():lerp_epsi(other, amount, eps) -end - ----------------------------------------------------------- -- vector products and projections ----------------------------------------------------------- -function vec2.dot(a, b) - return a.x * b.x + a.y * b.y +function vec2:dot(other) + return self.x * other.x + self.y * other.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 +function vec2:cross(other) + return self.x * other.y - self.y * other.x end ---scalar projection a onto b -function vec2.sproj(a, b) - local len = b:length() +function vec2:scalar_projection(other) + local len = other:length() if len == 0 then return 0 end - return a:dot(b) / len + return self:dot(other) / len end ---vector projection a onto b (writes into a) -function vec2.vproji(a, b) - local div = b:dot(b) +function vec2:vector_projection_inplace(other) + local div = other:dot(other) if div == 0 then - return a:sset(0,0) + return self:scalar_set(0) end - local fac = a:dot(b) / div - return a:vset(b):smuli(fac) + local fac = self:dot(other) / div + return self:vector_set(other):scalar_mul_inplace(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) +function vec2:vector_rejection_inplace(other) + local tx, ty = self.x, self.y + self:vector_projection_inplace(other) + self:scalar_set(tx - self.x, ty - self.y) + return self end --get the winding side of p, relative to the line a-b @@ -540,19 +416,19 @@ end ----------------------------------------------------------- --"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) +function vec2:apply_friction_inplace(mu, dt) + local friction = self:pooled_copy():scalar_mul_inplace(mu * dt) + if friction:length_squared() > self:length_squared() then + self:scalar_set(0, 0) else - self:vsubi(_v_friction) + self:vector_sub_inplace(friction) end + friction:release() return self end --"gamey" friction in one dimension -local function apply_friction_1d(v, mu, dt) +local function _friction_1d(v, mu, dt) local friction = mu * v * dt if math.abs(friction) > math.abs(v) then return 0 @@ -562,9 +438,9 @@ local function apply_friction_1d(v, mu, dt) 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) +function vec2:apply_friction_xy_inplace(mu_x, mu_y, dt) + self.x = _friction_1d(self.x, mu_x, dt) + self.y = _friction_1d(self.y, mu_y, dt) return self end @@ -578,7 +454,7 @@ function vec2:maxcomp() end -- mask out min component, with preference to keep x -function vec2:majori() +function vec2:major_inplace() if self.x > self.y then self.y = 0 else @@ -587,7 +463,7 @@ function vec2:majori() return self end -- mask out max component, with preference to keep x -function vec2:minori() +function vec2:minor_inplace() if self.x < self.y then self.y = 0 else @@ -596,14 +472,103 @@ function vec2:minori() return self end +--vector_ free alias; we're a vector library, so semantics should default to vector +vec2.add_inplace = vec2.vector_add_inplace +vec2.sub_inplace = vec2.vector_sub_inplace +vec2.mul_inplace = vec2.vector_mul_inplace +vec2.div_inplace = vec2.vector_div_inplace +vec2.set = vec2.vector_set ---garbage generating versions -function vec2:major(axis) - return self:copy():majori(axis) +--garbage generating functions that return a new vector rather than modifying self +for _, inplace_name in ipairs({ + "vector_add_inplace", + "vector_sub_inplace", + "vector_mul_inplace", + "vector_div_inplace", + "fused_multiply_add_inplace", + "add_inplace", + "sub_inplace", + "mul_inplace", + "div_inplace", + "scalar_add_inplace", + "scalar_sub_inplace", + "scalar_mul_inplace", + "scalar_div_inplace", + "normalise_both_inplace", + "normalise_inplace", + "normalise_len_inplace", + "inverse_inplace", + "rotate_inplace", + "rotate_around_inplace", + "rot90r_inplace", + "rot90l_inplace", + "lerp_direction_inplace", + "min_inplace", + "max_inplace", + "clamp_inplace", + "abs_inplace", + "sign_inplace", + "floor_inplace", + "ceil_inplace", + "round_inplace", + "lerp_inplace", + "lerp_eps_inplace", + "vector_projection_inplace", + "vector_rejection_inplace", + "apply_friction_inplace", + "apply_friction_xy_inplace", + "major_inplace", + "minor_inplace", +}) do + local garbage_name = inplace_name:gsub("_inplace", "") + vec2[garbage_name] = function(self, ...) + self = self:copy() + return self[inplace_name](self, ...) + end end -function vec2:minor(axis) - return self:copy():minori(axis) +--"hungarian" shorthand aliases for compatibility and short names +-- +--i do encourage using the longer versions above as it makes code easier +--to understand when you come back, but i also appreciate wanting short code +for _, v in ipairs({ + {"sset", "scalar_set"}, + {"sadd", "scalar_add"}, + {"ssub", "scalar_sub"}, + {"smul", "scalar_mul"}, + {"sdiv", "scalar_div"}, + {"vset", "vector_set"}, + {"vadd", "vector_add"}, + {"vsub", "vector_sub"}, + {"vmul", "vector_mul"}, + {"vdiv", "vector_div"}, + --(no plain addi etc, imo it's worth differentiating vaddi vs saddi) + {"fma", "fused_multiply_add"}, + {"vproj", "vector_projection"}, + {"vrej", "vector_rejection"}, + --just for the _inplace -> i shorthand, mostly for backwards compatibility + {"min", "min"}, + {"max", "max"}, + {"clamp", "clamp"}, + {"abs", "abs"}, + {"sign", "sign"}, + {"floor", "floor"}, + {"ceil", "ceil"}, + {"round", "round"}, + {"lerp", "lerp"}, + {"rotate", "rotate"}, + {"normalise", "normalise"}, +}) do + local shorthand, original = v[1], v[2] + if vec2[shorthand] == nil then + vec2[shorthand] = vec2[original] + end + --and inplace version + shorthand = shorthand .. "i" + original = original .. "_inplace" + if vec2[shorthand] == nil then + vec2[shorthand] = vec2[original] + end end return vec2