--- Tools for working with `` coordinates. -- @module Area.Position -- @usage local Position = require('__stdlib__/stdlib/area/position') -- @see Area.Area -- @see Concepts.Position -- @see defines.direction local Position = { __class = 'Position', __index = require('__stdlib__/stdlib/core') } setmetatable(Position, Position) local Direction = require('__stdlib__/stdlib/area/direction') local Orientation = require('__stdlib__/stdlib/area/orientation') local string = require('__stdlib__/stdlib/utils/string') local math = require('__stdlib__/stdlib/utils/math') local floor, abs, atan2, round_to, round = math.floor, math.abs, math.atan2, math.round_to, math.round local cos, sin, ceil, sqrt, pi, random = math.cos, math.sin, math.ceil, math.sqrt, math.pi, math.random local deg, acos, max, min, is_number = math.deg, math.acos, math.max, math.min, math.is_number local split = string.split local directions = defines.direction local AREA_PATH = '__stdlib__/stdlib/area/area' local EPSILON = 1.19e-07 local metatable --- Constructor Methods -- @section Constructors Position.__call = function(_, ...) local type = type((...)) if type == 'table' then local t = (...) if t.x and t.y then return Position.load(...) else return Position.new(...) end elseif type == 'string' then return Position.from_string(...) else return Position.construct(...) end end local function new(x, y) return setmetatable({ x = x, y = y }, metatable) end --- Returns a correctly formated position object. -- @usage Position.new({0, 0}) -- returns {x = 0, y = 0} -- @tparam Concepts.Position pos the position table or array to convert -- @treturn Concepts.Position function Position.new(pos) return new(pos.x or pos[1] or 0, pos.y or pos[2] or 0) end --- Creates a table representing the position from x and y. -- @tparam number x x-position -- @tparam number y y-position -- @treturn Concepts.Position function Position.construct(...) -- was self was passed as first argument? local args = type((...)) == 'table' and { select(2, ...) } or { select(1, ...) } return new(args[1] or 0, args[2] or args[1] or 0) end function Position.construct_xy(x, y) return new(x, y) end --- Update a position in place without returning a new position. -- @tparam Concepts.Position pos -- @tparam number x -- @tparam number y -- @return Concepts.Position the passed position updated. function Position.update(pos, x, y) pos.x, pos.y = x, y return pos end --- Load the metatable into the passed position without creating a new one. -- Always assumes a valid position is passed -- @tparam Concepts.Position pos the position to set the metatable onto -- @treturn Concepts.Position the position with metatable attached function Position.load(pos) return setmetatable(pos, metatable) end --- Converts a position string to a position. -- @tparam string pos_string the position to convert -- @treturn Concepts.Position function Position.from_string(pos_string) return Position(load('return ' .. pos_string)()) end --- Converts a string key position to a position. -- @tparam string pos_string the position to convert -- @treturn Concepts.Position function Position.from_key(pos_string) local tab = split(pos_string, ',', false, tonumber) return new(tab[1], tab[2]) end --- Gets the left top tile position of a chunk from the chunk position. -- @tparam Concepts.Position pos -- @treturn Concepts.Position function Position.from_chunk_position(pos) local x, y = (floor(pos.x) * 32), (floor(pos.y) * 32) return new(x, y) end --- Convert position from pixels -- @tparam Concepts.Position pos -- @treturn Concepts.Position pos function Position.from_pixels(pos) local x = pos.x / 32 local y = pos.y / 32 return new(x, y) end --- Position Methods -- @section Methods --- Addition of two positions. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position|number ... position or x, y values. -- @treturn Concepts.Position pos1 with pos2 added function Position.add(pos1, ...) pos1 = Position(pos1) local pos2 = Position(...) return new(pos1.x + pos2.x, pos1.y + pos2.y) end --- Subtraction of two positions.. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position|number ... position or x, y values -- @treturn Concepts.Position pos1 with pos2 subtracted function Position.subtract(pos1, ...) pos1 = Position(pos1) local pos2 = Position(...) return new(pos1.x - pos2.x, pos1.y - pos2.y) end --- Multiplication of two positions. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position|number ... position or x, y values -- @treturn Concepts.Position pos1 multiplied by pos2 function Position.multiply(pos1, ...) pos1 = Position(pos1) local pos2 = Position(...) return new(pos1.x * pos2.x, pos1.y * pos2.y) end --- Division of two positions. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position|number ... position or x, y values -- @treturn Concepts.Position pos1 divided by pos2 function Position.divide(pos1, ...) pos1 = Position(pos1) local pos2 = Position(...) return new(pos1.x / pos2.x, pos1.y / pos2.y) end --- Modulo of two positions. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position|number ... position or x, y values -- @treturn Concepts.Position pos1 modulo pos2 function Position.mod(pos1, ...) pos1 = Position(pos1) local pos2 = Position(...) return new(pos1.x % pos2.x, pos1.y % pos2.y) end --- Return the closest position to the first position. -- @tparam Concepts.Positions pos1 The position to find the closest too -- @tparam array positions array of Concepts.Position -- @treturn Concepts.Position function Position.closest(pos1, positions) local x, y = pos1.x, pos1.y local closest = math.MAXINT32 for _, pos in pairs(positions) do local distance = Position.distance(pos1, pos) if distance < closest then x, y = pos.x, pos.y closest = distance end end return new(x, y) end --- Return the farthest position from the first position. -- @tparam Concepts.Positions pos1 The position to find the farthest from -- @tparam array positions array of Concepts.Position -- @treturn Concepts.Position function Position.farthest(pos1, positions) local x, y = pos1.x, pos1.y local closest = 0 for _, pos in pairs(positions) do local distance = Position.distance(pos1, pos) if distance > closest then x, y = pos.x, pos.y closest = distance end end return new(x, y) end --- The middle of two positions. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @treturn Concepts.Position pos1 the middle of two positions function Position.between(pos1, pos2) return new((pos1.x + pos2.x) / 2, (pos1.y + pos2.y) / 2) end --- The projection point of two positions. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @treturn Concepts.Position pos1 projected function Position.projection(pos1, pos2) local s = (pos1.x * pos2.x + pos1.y * pos2.y) / (pos2.x * pos2.x + pos2.y * pos2.y) return new(s * pos2.x, s * pos2.y) end --- The reflection point or two positions. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @treturn Concepts.Position pos1 reflected function Position.reflection(pos1, pos2) local s = 2 * (pos1.x * pos2.x + pos1.y * pos2.y) / (pos2.x * pos2.x + pos2.y * pos2.y) return new(s * pos2.x - pos1.x, s * pos2.y - pos1.y) end --- Stores the position for recall later, not deterministic. -- Only the last position stored is saved. -- @tparam Concepts.Position pos function Position.store(pos) rawset(getmetatable(pos), '_saved', pos) return pos end --- Recalls the stored position. -- @tparam Concepts.Position pos -- @treturn Concepts.Position the stored position function Position.recall(pos) return rawget(getmetatable(pos), '_saved') end --- Normalizes a position by rounding it to 2 decimal places. -- @tparam Concepts.Position pos -- @treturn Concepts.Position a new normalized position function Position.normalize(pos) return new(round_to(pos.x, 2), round_to(pos.y, 2)) end --- Abs x, y values -- @tparam Concepts.Position pos -- @treturn Concepts.Position function Position.abs(pos) return new(abs(pos.x), abs(pos.y)) end --- Ceil x, y values. -- @tparam Concepts.Position pos -- @treturn Concepts.Position function Position.ceil(pos) return new(ceil(pos.x), ceil(pos.y)) end --- Floor x, y values. -- @tparam Concepts.Position pos -- @treturn Concepts.Position function Position.floor(pos) return new(floor(pos.x), floor(pos.y)) end local function pos_center(pos) local x, y local ceil_x = ceil(pos.x) local ceil_y = ceil(pos.y) x = pos.x >= 0 and floor(pos.x) + 0.5 or (ceil_x == pos.x and ceil_x + 0.5 or ceil_x - 0.5) y = pos.y >= 0 and floor(pos.y) + 0.5 or (ceil_y == pos.y and ceil_y + 0.5 or ceil_y - 0.5) return x, y end --- The center position of the tile where the given position resides. -- @tparam Concepts.Position pos -- @treturn Concepts.Position A new position at the center of the tile function Position.center(pos) return new(pos_center(pos)) end --- Rounds a positions points to the closest integer. -- @tparam Concepts.Position pos -- @treturn Concepts.Position A new position rounded function Position.round(pos) return new(round(pos.x), round(pos.y)) end --- Perpendicular position. -- @tparam Concepts.Position pos -- @treturn Concepts.Position pos function Position.perpendicular(pos) return new(-pos.y, pos.x) end --- Swap the x and y coordinates. -- @tparam Concepts.Position pos -- @treturn Concepts.Position A new position with x and y swapped function Position.swap(pos) return new(pos.y, pos.x) end --- Flip the signs of the position. -- @tparam Concepts.Position pos -- @return Concepts.Position A new position with flipped signs function Position.flip(pos) return new(-pos.x, -pos.y) end Position.unary = Position.flip --- Flip the x sign. -- @tparam Concepts.Position pos -- @return Concepts.Position A new position with flipped sign on the x function Position.flip_x(pos) return new(-pos.x, pos.y) end --- Flip the y sign. -- @tparam Concepts.Position pos -- @return Concepts.Position A new position with flipped sign on the y function Position.flip_y(pos) return new(pos.x, -pos.y) end --- Lerp position of pos1 and pos2. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @tparam float alpha 0-1 how close to get to position 2 -- @treturn Concepts.Position the lerped position function Position.lerp(pos1, pos2, alpha) local x = pos1.x + (pos2.x - pos1.x) * alpha local y = pos1.y + (pos2.y - pos1.y) * alpha return new(x, y) end --- Trim the position to a length. -- @tparam Concepts.Position pos -- @tparam number max_len function Position.trim(pos, max_len) local s = max_len * max_len / (pos.x * pos.x + pos.y * pos.y) s = (s > 1 and 1) or sqrt(s) return new(pos.x * s, pos.y * s) end --- Returns the position along line between source and target, at the distance from target. -- @tparam Concepts.Position pos1 where the line starts and extends from. -- @tparam Concepts.Position pos2 where the line ends and is offset back from. -- @tparam number distance_from_pos2 backwards from pos1 for the new position. -- @treturn Concepts.Position a point along line between source and target, at requested offset back from target. function Position.offset_along_line(pos1, pos2, distance_from_pos2) distance_from_pos2 = distance_from_pos2 or 0 local angle = Position.atan2(pos1, pos2) local veclength = Position.distance(pos1, pos2) - distance_from_pos2 -- From source_position, project the point along the vector at angle, and veclength local x = pos1.x + round_to(sin(angle) * veclength, 2) local y = pos1.y + round_to(cos(angle) * veclength, 2) return new(x, y) end --- Translates a position in the given direction. -- @tparam Concepts.Position pos the position to translate -- @tparam defines.direction direction the direction of translation -- @tparam number distance distance of the translation -- @treturn Concepts.Position a new translated position function Position.translate(pos, direction, distance) direction = direction or 0 distance = distance or 1 return Position.add(pos, Direction.to_vector(direction, distance)) end --- Return a random offset of a position. -- @tparam Concepts.Position pos the position to randomize -- @tparam[opt=0] number minimum the minimum amount to offset -- @tparam[opt=1] number maximum the maximum amount to offset -- @tparam[opt=false] boolean random_tile randomize the location on the tile -- @treturn Concepts.Position a new random offset position function Position.random(pos, minimum, maximum, random_tile) local rand_x = random(minimum or 0, maximum or 1) local rand_y = random(minimum or 0, maximum or 1) local x = pos.x + (random() >= .5 and -rand_x or rand_x) + (random_tile and random() or 0) local y = pos.y + (random() >= .5 and -rand_y or rand_y) + (random_tile and random() or 0) return new(x, y) end local function get_array(...) local array = select(2, ...) if array then table.insert(array, (...)) else array = (...) end return array end --- Return the average position of the passed positions. -- @tparam array positions array of Concepts.Position -- @treturn Concepts.Position a new position function Position.average(...) local positions = get_array(...) local avg = new(0, 0) for _, pos in ipairs(positions) do Position.add(avg, pos) end return Position.divide(avg, #positions) end --- Return the minimum position of the passed positions. -- @tparam array positions array of Concepts.Position -- @treturn Concepts.Position a new position function Position.min(...) local positions = get_array(...) local x, y local len = math.MAXINT32 for _, pos in pairs(positions) do local cur_len = Position.len(pos) if cur_len < len then len = cur_len x, y = pos.x, pos.y end end return new(x, y) end --- Return the maximum position of the passed positions. -- @tparam array positions array of Concepts.Position -- @treturn Concepts.Position a new position function Position.max(...) local positions = get_array(...) local x, y local len = -math.MAXINT32 for _, pos in pairs(positions) do local cur_len = Position.len(pos) if cur_len > len then len = cur_len x, y = pos.x, pos.y end end return new(x, y) end --- Return a position created from the smallest x, y values in the passed positions. -- @tparam array positions array of Concepts.Position -- @treturn Concepts.Position a new position function Position.min_xy(...) local positions = get_array(...) local x, y = positions[1].x, positions[1].y for _, pos in pairs(positions) do x = min(x, pos.x) y = min(y, pos.y) end return new(x, y) end --- Return a position created from the largest x, y values in the passed positions. -- @tparam array positions array of Concepts.Position -- @treturn Concepts.Position a new position function Position.max_xy(...) local positions = get_array(...) local x, y = positions[1].x, positions[1].y for _, pos in pairs(positions) do x = max(x, pos.x) y = max(y, pos.y) end return new(x, y) end --- The intersection of 4 positions. -- @treturn Concepts.Position a new position function Position.intersection(pos1_start, pos1_end, pos2_start, pos2_end) local d = (pos1_start.x - pos1_end.x) * (pos2_start.y - pos2_end.y) - (pos1_start.y - pos1_end.y) * (pos2_start.x - pos2_end.x) local a = pos1_start.x * pos1_end.y - pos1_start.y * pos1_end.x local b = pos2_start.x * pos2_end.y - pos2_start.y * pos2_end.x local x = (a * (pos2_start.x - pos2_end.x) - (pos1_start.x - pos1_end.x) * b) / d local y = (a * (pos2_start.y - pos2_end.y) - (pos1_start.y - pos1_end.y) * b) / d return is_number(x) and is_number(y) and new(x, y) or pos1_start end --- Position Mutate Methods -- @section Mutate Methods --- Normalizes a position by rounding it to 2 decimal places. -- @tparam Concepts.Position pos -- @treturn Concepts.Position the normalized position mutated function Position.normalized(pos) pos.x, pos.y = round_to(pos.x, 2), round_to(pos.y, 2) return pos end --- Abs x, y values -- @tparam Concepts.Position pos -- @treturn Concepts.Position the absolute position mutated function Position.absed(pos) pos.x, pos.y = abs(pos.x), abs(pos.y) return pos end --- Ceil x, y values in place. -- @tparam Concepts.Position pos -- @treturn Concepts.Position the ceiled position mutated function Position.ceiled(pos) pos.x, pos.y = ceil(pos.x), ceil(pos.y) return pos end --- Floor x, y values. -- @tparam Concepts.Position pos -- @treturn Concepts.Position the floored position mutated function Position.floored(pos) pos.x, pos.y = floor(pos.x), floor(pos.y) return pos end --- The center position of the tile where the given position resides. -- @tparam Concepts.Position pos -- @treturn Concepts.Position the centered position mutated function Position.centered(pos) pos.x, pos.y = pos_center(pos) return pos end --- Rounds a positions points to the closest integer. -- @tparam Concepts.Position pos -- @treturn Concepts.Position the rounded position mutated function Position.rounded(pos) pos.x, pos.y = round(pos.x), round(pos.y) return pos end --- Swap the x and y coordinates. -- @tparam Concepts.Position pos -- @treturn Concepts.Position the swapped position mutated function Position.swapped(pos) pos.x, pos.y = pos.y, pos.x return pos end --- Flip the signs of the position. -- @tparam Concepts.Position pos -- @return Concepts.Position the flipped position mutated function Position.flipped(pos) pos.x, pos.y = -pos.x, -pos.y return pos end --- Position Conversion Methods -- @section Position Conversion Methods -- Test Comment --- Convert to pixels from position -- @tparam Concepts.Position pos -- @treturn Concepts.Position pos function Position.to_pixels(pos) local x = pos.x * 32 local y = pos.y * 32 return new(x, y) end --- Gets the chunk position of a chunk where the specified position resides. -- @tparam Concepts.Position pos a position residing somewhere in a chunk -- @treturn Concepts.ChunkPosition a new chunk position -- @usage local chunk_x = Position.chunk_position(pos).x function Position.to_chunk_position(pos) local x, y = floor(pos.x / 32), floor(pos.y / 32) return new(x, y) end --- Area Conversion Methods -- @section Area Conversion Methods -- Hackish function, Factorio lua doesn't allow require inside functions because... local function load_area(area) local Area = package.loaded[AREA_PATH] if not Area then local log = log or function(_msg_) end log('WARNING: Area for Position not found in package.loaded') end return Area and Area.load(area) or area end --- Expands a position to a square area. -- @tparam Concepts.Position pos the position to expand into an area -- @tparam number radius half of the side length of the area -- @treturn Concepts.BoundingBox the area function Position.expand_to_area(pos, radius) radius = radius or 1 local left_top = { x = pos.x - radius, y = pos.y - radius } local right_bottom = { x = pos.x + radius, y = pos.y + radius } return load_area { left_top = left_top, right_bottom = right_bottom } end --- Expands a position into an area by setting pos to left_top. -- @tparam Concepts.Position pos -- @tparam number width -- @tparam number height -- @treturn Concepts.BoundingBox function Position.to_area(pos, width, height) width = width or 0 height = height or width local left_top = { x = pos.x, y = pos.y } local right_bottom = { x = pos.x + width, y = pos.y + height } return load_area { left_top = left_top, right_bottom = right_bottom } end --- Converts a tile position to the @{Concepts.BoundingBox|area} of the tile it is in. -- @tparam LuaTile.position pos the tile position -- @treturn Concepts.BoundingBox the area of the tile function Position.to_tile_area(pos) local x, y = floor(pos.x), floor(pos.y) local left_top = { x = x, y = y } local right_bottom = { x = x + 1, y = y + 1 } return load_area { left_top = left_top, right_bottom = right_bottom } end --- Get the chunk area the specified position is in. -- @tparam Concepts.Position pos -- @treturn Concepts.BoundingBox function Position.to_chunk_area(pos) local left_top = { x = floor(pos.x / 32) * 32, y = floor(pos.y / 32) * 32 } local right_bottom = { x = left_top.x + 32, y = left_top.y + 32 } return load_area { left_top = left_top, right_bottom = right_bottom } end --- Get the chunk area for the specified chunk position. -- @tparam Concepts.ChunkPosition pos -- @treturn Concepts.BoundingBox The chunks positions area function Position.chunk_position_to_chunk_area(pos) local left_top = { x = pos.x * 32, y = pos.y * 32 } local right_bottom = { left_top.x + 32, left_top.y + 32 } return load_area { left_top = left_top, right_bottom = right_bottom } end --- Position Functions -- @section Functions --- Gets the squared length of a position -- @tparam Concepts.Position pos -- @treturn number function Position.len_squared(pos) return pos.x * pos.x + pos.y * pos.y end --- Gets the length of a position -- @tparam Concepts.Position pos -- @treturn number function Position.len(pos) return (pos.x * pos.x + pos.y * pos.y) ^ 0.5 end --- Converts a position to a string. -- @tparam Concepts.Position pos the position to convert -- @treturn string string representation of the position function Position.to_string(pos) return '{x = ' .. pos.x .. ', y = ' .. pos.y .. '}' end --- Converts a position to an x, y string. -- @tparam Concepts.Position pos the position to convert -- @treturn string function Position.to_string_xy(pos) return pos.x .. ', ' .. pos.y end --- Converts a position to a string suitable for using as a table index. -- @tparam Concepts.Position pos the position to convert -- @treturn string function Position.to_key(pos) return pos.x .. ',' .. pos.y end --- Unpack a position into a tuple. -- @tparam Concepts.Position pos the position to unpack -- @treturn tuple x, y function Position.unpack(pos) return pos.x, pos.y end --- Packs a position into an array. -- @tparam Concepts.Position pos the position to pack -- @treturn array function Position.pack(pos) return { pos.x, pos.y } end --- Is this position {0, 0}. -- @tparam Concepts.Position pos -- @treturn boolean function Position.is_zero(pos) return pos.x == 0 and pos.y == 0 end --- Is a position inside of an area. -- @tparam Concepts.Position pos The pos to check -- @tparam Concepts.BoundingBox area The area to check. -- @treturn boolean Is the position inside of the area. function Position.inside(pos, area) local lt = area.left_top local rb = area.right_bottom return pos.x >= lt.x and pos.y >= lt.y and pos.x <= rb.x and pos.y <= rb.y end --- Is this a simple position. {num, num} -- @tparam Concepts.Position pos -- @treturn boolean function Position.is_simple_position(pos) return type(pos) == 'table' and type(pos[1]) == 'number' and type(pos[2]) == 'number' end --- Is this a complex position. {x = number, y = number} -- @tparam Concepts.Position pos -- @treturn boolean function Position.is_complex_position(pos) return type(pos) == 'table' and type(pos.x) == 'number' and type(pos.y) == 'number' end --- Does the position have the class attached -- @tparam Concepts.Position pos -- @treturn boolean function Position.is_Position(pos) return getmetatable(pos) == metatable end --- Is this any position -- @tparam Concepts.Position pos -- @treturn boolean function Position.is_position(pos) return Position.is_Position(pos) or Position.is_complex_position(pos) or Position.is_simple_position(pos) end --- Return the atan2 of 2 positions. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @treturn number function Position.atan2(pos1, pos2) return atan2(pos2.x - pos1.x, pos2.y - pos1.y) end --- The angle between two positions -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @treturn number function Position.angle(pos1, pos2) local dist = Position.distance(pos1, pos2) if dist ~= 0 then return deg(acos((pos1.x * pos2.x + pos1.y * pos2.y) / dist)) else return 0 end end --- Return the cross product of two positions. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @treturn number function Position.cross(pos1, pos2) return pos1.x * pos2.y - pos1.y * pos2.x end -- Return the dot product of two positions. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @treturn number function Position.dot(pos1, pos2) return pos1.x * pos2.x + pos1.y * pos2.y end --- Tests whether or not the two given positions are equal. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @treturn boolean true if positions are equal function Position.equals(pos1, pos2) if not (pos1 and pos2) then return false end return abs(pos1.x - pos2.x) < EPSILON and abs(pos1.y - pos2.y) < EPSILON end --- Is pos1 less than pos2. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @treturn boolean function Position.less_than(pos1, pos2) return Position.len(pos1) < Position.len(pos2) end --- Is pos1 less than or equal to pos2. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @treturn boolean function Position.less_than_eq(pos1, pos2) return Position.len(pos1) <= Position.len(pos2) end --- Calculates the Euclidean distance squared between two positions, useful when sqrt is not needed. -- @tparam Concepts.Position pos1 -- @tparam[opt] Concepts.Position pos2 -- @treturn number the square of the euclidean distance function Position.distance_squared(pos1, pos2) local ax_bx = pos1.x - pos2.x local ay_by = pos1.y - pos2.y return ax_bx * ax_bx + ay_by * ay_by end --- Calculates the Euclidean distance between two positions. -- @tparam Concepts.Position pos1 -- @tparam[opt={x=0, y=0}] Concepts.Position pos2 -- @treturn number the euclidean distance function Position.distance(pos1, pos2) local ax_bx = pos1.x - pos2.x local ay_by = pos1.y - pos2.y return (ax_bx * ax_bx + ay_by * ay_by) ^ 0.5 end --- Calculates the manhatten distance between two positions. -- @tparam Concepts.Position pos1 -- @tparam[opt] Concepts.Position pos2 the second position -- @treturn number the manhatten distance -- @see https://en.wikipedia.org/wiki/Taxicab_geometry Taxicab geometry (manhatten distance) function Position.manhattan_distance(pos1, pos2) return abs(pos2.x - pos1.x) + abs(pos2.y - pos1.y) end --- Returms the direction to a position using simple delta comparisons. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @treturn defines.direction function Position.direction_to(pos1, pos2) local dx = pos1.x - pos2.x local dy = pos1.y - pos2.y if dx ~= 0 then if dy == 0 then return dx > 0 and directions.west or directions.east else local adx, ady = abs(dx), abs(dy) if adx > ady then return dx > 0 and directions.north or directions.south else return dy > 0 and directions.west or directions.east end end else return dy > 0 and directions.north or directions.south end end --- Returns the direction to a position. -- @tparam Concepts.Position pos1 -- @tparam Concepts.Position pos2 -- @tparam boolean eight_way return the eight way direction -- @treturn defines.direction function Position.complex_direction_to(pos1, pos2, eight_way) return Orientation.to_direction(Position.orientation_to(pos1, pos2), eight_way) end function Position.orientation_to(pos1, pos2) return (1 - (Position.atan2(pos1, pos2) / pi)) / 2 end --- Increment a position each time it is called. -- This can be used to increment or even decrement a position quickly. --

Do not store function closures in the global object; use them in the current tick. -- @usage -- local next_pos = Position.increment({0,0}) -- for i = 1, 5 do next_pos(0,1) -- returns {x = 0, y = 1} {x = 0, y = 2} {x = 0, y = 3} {x = 0, y = 4} {x = 0, y = 5} -- @usage -- local next_pos = Position.increment({0, 0}, 1) -- next_pos() -- returns {1, 0} -- next_pos(0, 5) -- returns {1, 5} -- next_pos(nil, 5) -- returns {2, 10} -- @usage -- local next_pos = Position.increment({0, 0}, 0, 1) -- surface.create_entity{name = 'flying-text', text = 'text', position = next_pos()} -- surface.create_entity{name = 'flying-text', text = 'text', position = next_pos()} -- creates two flying text entities 1 tile apart -- @tparam Concepts.Position pos the position to start with -- @tparam[opt=0] number inc_x optional increment x by this amount -- @tparam[opt=0] number inc_y optional increment y by this amount -- @tparam[opt=false] boolean increment_initial Whether the first use should be incremented -- @treturn function @{increment_closure} a function closure that returns a new incremented position function Position.increment(pos, inc_x, inc_y, increment_initial) local x, y = pos.x, pos.y inc_x, inc_y = inc_x or 0, inc_y or 0 --- A closure which the @{increment} function returns. -- @function increment_closure -- > Do not call this directly and do not store this in the global object. -- @see increment -- @tparam[opt=0] number new_inc_x -- @tparam[opt=0] number new_inc_y -- @treturn Concepts.Position the incremented position return function(new_inc_x, new_inc_y) if increment_initial then x = x + (new_inc_x or inc_x) y = y + (new_inc_y or inc_y) else x = x y = y increment_initial = true end return new(x, y) end end --- Metamethods -- @section Metamethods --- Position tables are returned with these metamethods attached. -- Methods that return a position will return a NEW position without modifying the passed positions. -- @table Metamethods metatable = { __class = 'position', __index = Position, -- If key is not found, see if there is one availble in the Position module. __add = Position.add, -- Adds two position together. Returns a new position. __sub = Position.subtract, -- Subtracts one position from another. Returns a new position. __mul = Position.multiply, -- Multiply 2 positions. Returns a new position. __div = Position.divide, -- Divide 2 positions. Returns a new position. __mod = Position.mod, -- Modulo of 2 positions. Returns a new position. __unm = Position.flip, -- Unary Minus of a position. Returns a new position. __len = Position.len, -- Length of a single position. __eq = Position.equals, -- Are two positions at the same spot. __lt = Position.less_than, -- Is position1 less than position2. __le = Position.less_than_eq, -- Is position1 less than or equal to position2. __tostring = Position.to_string, -- Returns a string representation of the position __concat = _ENV.concat, -- calls tostring on both sides of concact. __call = Position.new, -- copy the position. __debugline = [[{[}x={x},y={y}{]}]] } return Position